html_bindgen/generate/html/
mod.rs

1use super::{CodeFile, ModuleMapping};
2use crate::merge::{MergedCategory, MergedElement};
3use crate::parse::{Attribute, AttributeType};
4use crate::{utils, Result};
5use builder::gen_builder;
6use indoc::formatdoc;
7
8mod builder;
9
10pub fn generate(
11    parsed: impl Iterator<Item = Result<MergedElement>>,
12    global_attributes: &[Attribute],
13    module_map: &[super::ModuleMapping],
14) -> Result<Vec<CodeFile>> {
15    let mut output = vec![];
16    let mut tag_names = vec![];
17
18    // generate individual `{element}.rs` files
19    for el in parsed {
20        let el = el?;
21        tag_names.push(el.tag_name.clone());
22        output.push(generate_element(el, global_attributes)?);
23    }
24    tag_names.sort();
25
26    let mods = tag_names
27        .iter()
28        .map(|tag_name| format!("pub(crate) mod {tag_name};"))
29        .collect::<String>();
30
31    let all_files = tag_names
32        .iter()
33        .map(|tag_name| format!("pub(crate) use crate::generated::{tag_name}::element::*;"))
34        .collect::<String>();
35
36    let all_builders = tag_names
37        .iter()
38        .map(|tag_name| format!("pub(crate) use crate::generated::{tag_name}::builder::*;"))
39        .collect::<String>();
40
41    let all_children = tag_names
42        .iter()
43        .map(|tag_name| format!("pub(crate) use crate::generated::{tag_name}::child::*;"))
44        .collect::<String>();
45
46    let by_mapping = module_map
47        .iter()
48        .map(|ModuleMapping { name, children }| {
49            let elements = children
50                .iter()
51                .map(|tag_name| format!("pub use crate::generated::{tag_name}::element::*;"))
52                .collect::<String>();
53            let builders = children
54                .iter()
55                .map(|tag_name| format!("pub use crate::generated::{tag_name}::builder::*;"))
56                .collect::<String>();
57            let children = children
58                .iter()
59                .map(|tag_name| format!("pub use crate::generated::{tag_name}::child::*;"))
60                .collect::<String>();
61
62            format!(
63                "
64                pub mod {name} {{
65                    pub mod elements {{
66                        {elements}
67                    }}
68                    /// Child elements
69                    pub mod children {{
70                        {children}
71                    }}
72                    /// Element builders
73                    pub mod builders {{
74                        {builders}
75                    }}
76                }}
77            "
78            )
79        })
80        .collect::<String>();
81
82    let code = format!(
83        "//! HTML elements support
84        {mods}
85
86        /// All auto-generated items in this crate
87        #[allow(unused)]
88        pub(crate) mod all {{
89            {all_files}
90
91            /// All auto-generated builders
92            pub mod builders {{
93                {all_builders}
94            }}
95
96            /// All auto-generated children
97            pub mod children {{
98                {all_children}
99            }}
100        }}
101
102        /// Modules according to the MDN mappings.
103        pub(crate) mod mdn {{
104            {by_mapping}
105        }}
106        "
107    );
108    output.push(CodeFile {
109        filename: "mod.rs".to_owned(),
110        code: utils::fmt(&code)?,
111        dir: String::new(),
112    });
113
114    Ok(output)
115}
116
117/// Generate a single element.
118fn generate_element(el: MergedElement, global_attributes: &[Attribute]) -> Result<CodeFile> {
119    let MergedElement {
120        tag_name,
121        struct_name,
122        attributes,
123        mdn_link,
124        has_global_attributes,
125        submodule_name,
126        content_categories,
127        permitted_child_elements,
128        has_closing_tag,
129        ..
130    } = el;
131
132    let filename = format!("{}.rs", tag_name);
133    let enum_name = format!("super::child::{struct_name}Child");
134    let sys_name = format!("html_sys::{submodule_name}::{struct_name}");
135
136    let should_indent = match tag_name.as_str() {
137        "pre" => false,
138        _ => true,
139    };
140
141    let has_children = permitted_child_elements.len() != 0;
142    let categories_impl = gen_categories_impl(&content_categories, &struct_name);
143    let html_element_impl = gen_html_element_impl(&struct_name, has_global_attributes);
144    let children_enum = gen_enum(&struct_name, &permitted_child_elements, should_indent);
145    let child_methods = gen_child_methods(&struct_name, &enum_name, &permitted_child_elements);
146    let data_map_methods = gen_data_map_methods(&struct_name);
147    let display_impl = gen_fmt_impl(&struct_name, has_children, has_closing_tag, should_indent);
148
149    let method_attributes = match has_global_attributes {
150        true => {
151            let mut attrs = attributes.clone();
152            attrs.append(&mut global_attributes.to_owned());
153            attrs
154        }
155        false => attributes.clone(),
156    };
157    let builder = gen_builder(&struct_name, &permitted_child_elements, &method_attributes);
158    let getter_setter_methods = gen_methods(&struct_name, &method_attributes);
159
160    let children = match has_children {
161        true => format!("children: Vec<{enum_name}>"),
162        false => String::new(),
163    };
164
165    let gen_children = match has_children {
166        true => "children: vec![]".to_owned(),
167        false => String::new(),
168    };
169
170    let element = formatdoc!(
171        r#"/// The HTML `<{tag_name}>` element
172        ///
173        /// [MDN Documentation]({mdn_link})
174        #[doc(alias = "{tag_name}")]
175        #[non_exhaustive]
176        #[derive(PartialEq, Clone, Default)]
177        pub struct {struct_name} {{
178            sys: {sys_name},
179            {children}
180        }}
181
182        impl {struct_name} {{
183            /// Create a new builder
184            pub fn builder() -> super::builder::{struct_name}Builder {{
185                super::builder::{struct_name}Builder::new(Default::default())
186            }}
187        }}
188
189        {data_map_methods}
190        {getter_setter_methods}
191        {child_methods}
192
193        {display_impl}
194        {html_element_impl}
195        {categories_impl}
196
197        impl std::convert::Into<{sys_name}> for {struct_name} {{
198            fn into(self) -> {sys_name} {{
199                self.sys
200            }}
201        }}
202
203        impl From<{sys_name}> for {struct_name} {{
204            fn from(sys: {sys_name}) -> Self {{
205                Self {{
206                    sys,
207                    {gen_children}
208                }}
209            }}
210        }}
211    "#
212    );
213
214    let code = format!(
215        "
216        pub mod element {{
217            {element}
218        }}
219
220        pub mod child {{
221            {children_enum}
222        }}
223
224        pub mod builder {{
225            {builder}
226        }}
227    "
228    );
229
230    Ok(CodeFile {
231        filename,
232        code: utils::fmt(&code)?,
233        dir: "".to_owned(),
234    })
235}
236
237fn gen_fmt_impl(
238    struct_name: &str,
239    has_children: bool,
240    has_closing_tag: bool,
241    should_indent: bool,
242) -> String {
243    let write_debug_children = if has_children && should_indent {
244        format!(
245            r#"
246            if !self.children.is_empty() {{
247                write!(f, "\n")?;
248            }}
249            for el in &self.children {{
250                crate::Render::render(&el, f, depth)?;
251                write!(f, "\n")?;
252            }}"#
253        )
254    } else if has_children && !should_indent {
255        format!(
256            r#"
257            for el in &self.children {{
258                crate::Render::render(&el, f, 0)?;
259            }}"#
260        )
261    } else {
262        String::new()
263    };
264
265    let write_display_children = if has_children {
266        format!(
267            r#"
268            for el in &self.children {{
269                write!(f, "{{el}}")?;
270            }}"#
271        )
272    } else {
273        String::new()
274    };
275
276    let write_closing_tag = if has_closing_tag && should_indent {
277        r#"
278            write!(f, "{:level$}", "", level = depth * 4)?;
279            html_sys::RenderElement::write_closing_tag(&self.sys, f)?;
280            "#
281    } else if has_closing_tag && !should_indent {
282        r#"
283            html_sys::RenderElement::write_closing_tag(&self.sys, f)?;
284            "#
285    } else {
286        ""
287    };
288    format!(
289        r#"
290        impl crate::Render for {struct_name} {{
291            fn render(&self, f: &mut std::fmt::Formatter<'_>, depth: usize) -> std::fmt::Result {{
292                write!(f, "{{:level$}}", "", level = depth * 4)?;
293                html_sys::RenderElement::write_opening_tag(&self.sys, f)?;
294                {write_debug_children}
295                {write_closing_tag}
296                Ok(())
297            }}
298        }}
299
300        impl std::fmt::Debug for {struct_name} {{
301            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{
302                crate::Render::render(self, f, 0)?;
303                Ok(())
304            }}
305        }}
306
307        impl std::fmt::Display for {struct_name} {{
308            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{
309                html_sys::RenderElement::write_opening_tag(&self.sys, f)?;
310                {write_display_children}
311                html_sys::RenderElement::write_closing_tag(&self.sys, f)?;
312                Ok(())
313            }}
314        }}
315    "#
316    )
317}
318
319fn gen_child_methods(
320    struct_name: &str,
321    enum_name: &str,
322    permitted_child_elements: &[String],
323) -> String {
324    if permitted_child_elements.len() == 0 {
325        return String::new();
326    }
327
328    format!(
329        "impl {struct_name} {{
330            /// Access the element's children
331            pub fn children(&self) -> &[{enum_name}] {{
332                self.children.as_ref()
333            }}
334
335            /// Mutably access the element's children
336            pub fn children_mut(&mut self) -> &mut Vec<{enum_name}> {{
337                &mut self.children
338            }}
339        }}"
340    )
341}
342
343fn gen_data_map_methods(struct_name: &str) -> String {
344    format!(
345        "impl {struct_name} {{
346            /// Access the element's `data-*` properties
347            pub fn data_map(&self) -> &html_sys::DataMap {{
348                &self.sys.data_map
349            }}
350
351            /// Mutably access the element's `data-*` properties
352            pub fn data_map_mut(&mut self) -> &mut html_sys::DataMap {{
353                &mut self.sys.data_map
354            }}
355        }}"
356    )
357}
358
359fn gen_enum(struct_name: &str, permitted_child_elements: &[String], should_indent: bool) -> String {
360    if permitted_child_elements.len() == 0 {
361        return String::new();
362    }
363
364    /// Take an element type, and convert it to a path
365    /// In the case of the special `Text` type, we use a
366    /// Rust `String`.
367    fn gen_ty_path(el: &str) -> String {
368        if el == "Text" {
369            "std::borrow::Cow<'static, str>".to_owned()
370        } else {
371            format!("crate::generated::all::{el}")
372        }
373    }
374
375    let members = permitted_child_elements
376        .iter()
377        .map(|el| {
378            let ty = gen_ty_path(el);
379            format!(
380                "/// The {el} element
381                {el}({ty}),"
382            )
383        })
384        .collect::<String>();
385
386    let from = permitted_child_elements
387        .iter()
388        .map(|el| {
389            let ty = gen_ty_path(el);
390
391            let base_impl = format!(
392                "
393            impl std::convert::From<{ty}> for {struct_name}Child {{
394                fn from(value: {ty}) -> Self {{
395                    Self::{el}(value)
396                }}
397            }}
398        "
399            );
400            if ty.contains("borrow") {
401                format!(
402                    "
403                    {base_impl}
404
405                    impl std::convert::From<&'static str> for {struct_name}Child {{
406                        fn from(value: &'static str) -> Self {{
407                            Self::{el}(value.into())
408                        }}
409                    }}
410                    impl std::convert::From<String> for {struct_name}Child {{
411                        fn from(value: String) -> Self {{
412                            Self::{el}(value.into())
413                        }}
414                    }}
415                "
416                )
417            } else {
418                base_impl
419            }
420        })
421        .collect::<String>();
422
423    let increase_depth = match should_indent {
424        true => "+ 1",
425        false => "",
426    };
427    let debug_patterns = permitted_child_elements
428        .iter()
429        .map(|el| {
430            format!(r#"Self::{el}(el) => crate::Render::render(el, f, depth {increase_depth}),"#)
431        })
432        .collect::<String>();
433    let display_patterns = permitted_child_elements
434        .iter()
435        .map(|el| format!(r#"Self::{el}(el) => write!(f, "{{el}}"),"#))
436        .collect::<String>();
437
438    format!(
439        r#"
440        /// The permitted child items for the `{struct_name}` element
441        #[derive(PartialEq, Clone)]
442        pub enum {struct_name}Child {{
443            {members}
444        }}
445        {from}
446
447        impl crate::Render for {struct_name}Child {{
448            fn render(&self, f: &mut std::fmt::Formatter<'_>, depth: usize) -> std::fmt::Result {{
449                match self {{
450                    {debug_patterns}
451                }}
452            }}
453        }}
454
455        impl std::fmt::Debug for {struct_name}Child {{
456            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{
457                crate::Render::render(self, f, 0)?;
458                Ok(())
459            }}
460        }}
461
462        impl std::fmt::Display for {struct_name}Child {{
463            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{
464                match self {{
465                    {display_patterns}
466                }}
467            }}
468        }}
469        "#
470    )
471}
472
473fn gen_html_element_impl(struct_name: &str, has_global_attributes: bool) -> String {
474    if has_global_attributes {
475        format!(
476            "
477            impl crate::HtmlElement for {struct_name} {{}}
478        "
479        )
480    } else {
481        String::new()
482    }
483}
484
485fn gen_categories_impl(categories: &[MergedCategory], struct_name: &str) -> String {
486    let mut output = String::new();
487    for cat in categories {
488        generate_category(cat, &mut output, struct_name);
489    }
490    output
491}
492
493fn generate_category(cat: &MergedCategory, output: &mut String, struct_name: &str) {
494    match cat {
495        MergedCategory::Metadata => output.push_str(&format!(
496            "impl crate::MetadataContent for {struct_name} {{}}"
497        )),
498        MergedCategory::Flow => {
499            output.push_str(&format!("impl crate::FlowContent for {struct_name} {{}}"))
500        }
501        MergedCategory::Sectioning => {
502            output.push_str(&format!(
503                "impl crate::SectioningContent for {struct_name} {{}}"
504            ));
505            // generate_category(&Category::Flow, output, struct_name);
506        }
507        MergedCategory::Heading => {
508            output.push_str(&format!(
509                "impl crate::HeadingContent for {struct_name} {{}}"
510            ));
511            // generate_category(&Category::Flow, output, struct_name);
512        }
513        MergedCategory::Phrasing => {
514            output.push_str(&format!(
515                "impl crate::PhrasingContent for {struct_name} {{}}"
516            ));
517            // generate_category(&Category::Flow, output, struct_name);
518        }
519        MergedCategory::Embedded => {
520            output.push_str(&format!(
521                "impl crate::EmbeddedContent for {struct_name} {{}}"
522            ));
523            // generate_category(&Category::Flow, output, struct_name);
524        }
525        MergedCategory::Interactive => {
526            output.push_str(&format!(
527                "impl crate::InteractiveContent for {struct_name} {{}}"
528            ));
529            // generate_category(&Category::Flow, output, struct_name);
530        }
531        MergedCategory::Palpable => output.push_str(&format!(
532            "impl crate::PalpableContent for {struct_name} {{}}"
533        )),
534        MergedCategory::ScriptSupporting => output.push_str(&format!(
535            "impl crate::ScriptSupportingContent for {struct_name} {{}}"
536        )),
537        MergedCategory::Transparent => output.push_str(&format!(
538            "impl crate::TransparentContent for {struct_name} {{}}"
539        )),
540    }
541}
542
543fn gen_methods(struct_name: &str, attributes: &[Attribute]) -> String {
544    fn gen_method(attr: &Attribute) -> String {
545        let name = &attr.name;
546        let field_name = &attr.field_name;
547        let return_ty = match &attr.ty {
548            AttributeType::Bool => "bool".to_owned(),
549            AttributeType::String => "std::option::Option<&str>".to_owned(),
550            ty => format!("std::option::Option<{ty}>"),
551        };
552
553        let param_ty = match &attr.ty {
554            AttributeType::Bool => "bool".to_owned(),
555            AttributeType::String => {
556                "std::option::Option<impl Into<std::borrow::Cow<'static, str>>>".to_owned()
557            }
558            ty => format!("std::option::Option<{ty}>"),
559        };
560
561        let field_access = match &attr.ty {
562            AttributeType::Integer | AttributeType::Float | AttributeType::Bool => {
563                format!("self.sys.{field_name}")
564            }
565            AttributeType::String => {
566                format!("self.sys.{field_name}.as_deref()")
567            }
568            _ => todo!("unhandled type"),
569        };
570        let field_setter = match &attr.ty {
571            AttributeType::String => format!("value.map(|v| v.into())"),
572            _ => format!("value"),
573        };
574        format!(
575            "
576            /// Get the value of the `{name}` attribute
577            pub fn {field_name}(&self) -> {return_ty} {{
578                {field_access}
579            }}
580            /// Set the value of the `{name}` attribute
581            pub fn set_{field_name}(&mut self, value: {param_ty}) {{
582                self.sys.{field_name} = {field_setter};
583            }}",
584        )
585    }
586    let methods: String = attributes.into_iter().map(gen_method).collect();
587
588    match methods.len() {
589        0 => String::new(),
590        _ => format!(
591            "
592            impl {struct_name} {{
593                {methods}
594            }}
595        "
596        ),
597    }
598}