html_bindgen/generate/sys/
mod.rs

1use super::{CodeFile, Module};
2use std::fmt::Write;
3use std::{collections::HashMap, iter};
4
5use crate::merge::MergedElement;
6use crate::parse::{Attribute, AttributeType};
7use crate::{utils, Result};
8use indoc::{formatdoc, writedoc};
9
10const INCLUDES: &str = r##"
11/// Render an element to a writer.
12pub trait RenderElement {
13    /// Write the opening tag to a writer.
14    fn write_opening_tag<W: std::fmt::Write >(&self, writer: &mut W) -> std::fmt::Result;
15
16    /// Write the closing tag to a writer, if one is available.
17    fn write_closing_tag<W: std::fmt::Write >(&self, writer: &mut W) -> std::fmt::Result;
18}
19
20/// Container for `data-*` attributes.
21#[derive(Debug, Clone, PartialEq, Default)]
22pub struct DataMap {
23    map: std::collections::HashMap<std::borrow::Cow<'static, str>, std::borrow::Cow<'static, str>>,
24}
25
26impl std::ops::Deref for DataMap {
27    type Target = std::collections::HashMap<std::borrow::Cow<'static, str>, std::borrow::Cow<'static, str>>;
28
29    fn deref(&self) -> &Self::Target {
30        &self.map
31    }
32}
33
34impl std::ops::DerefMut for DataMap {
35    fn deref_mut(&mut self) -> &mut Self::Target {
36        &mut self.map
37    }
38}
39
40impl std::fmt::Display for DataMap {
41    fn fmt(&self, writer: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        for (key, value) in self.map.iter() {
43            write!(writer, r#" data-{key}="{value}""#)?;
44        }
45        Ok(())
46    }
47}
48"##;
49
50pub fn generate(
51    merged: impl Iterator<Item = Result<MergedElement>>,
52    global_attributes: &[Attribute],
53    modules: &[Module],
54) -> Result<Vec<CodeFile>> {
55    let mut output = vec![];
56    let mut generated: HashMap<String, Vec<String>> = HashMap::new();
57
58    // generate individual `{element}.rs` files
59    for el in merged {
60        let el = el?;
61        let entry = generated.entry(el.submodule_name.clone());
62        entry.or_default().push(el.tag_name.clone());
63        let cf = generate_element(el)?;
64        output.push(cf);
65    }
66
67    // generate `mod.rs` files
68    let mut dirs = vec![];
69    for (dir, mut filenames) in generated {
70        filenames.sort();
71        dirs.push(dir.clone());
72        let code = filenames
73            .into_iter()
74            .map(|name| format!("mod {name};\npub use {name}::*;"))
75            .collect::<String>();
76
77        let module = modules.iter().find(|el| &el.name == &dir).unwrap();
78        let description = &module.description;
79        let code = format!(
80            "//! {description}
81            {code}"
82        );
83
84        output.push(CodeFile {
85            filename: "mod.rs".to_owned(),
86            code: utils::fmt(&code).expect("could not parse code"),
87            dir,
88        })
89    }
90    dirs.sort();
91
92    // generate `lib.rs` file
93    let code = dirs
94        .into_iter()
95        .map(|d| format!("pub mod {d};\n"))
96        .chain(iter::once(INCLUDES.to_owned()))
97        .chain(iter::once({
98            let fields = generate_fields(global_attributes);
99
100            let mut display_attrs = String::new();
101            for attr in global_attributes {
102                display_attrs.push_str(&generate_attribute_display(&attr));
103            }
104            formatdoc!(
105                r#"
106
107                    /// The "global attributes" struct
108                    #[derive(Debug, Clone, PartialEq, Default)]
109                    pub struct GlobalAttributes {{
110                        {fields}
111                    }}
112
113                    impl std::fmt::Display for GlobalAttributes {{
114                        fn fmt(&self, writer: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{
115                            {display_attrs}
116                            Ok(())
117                        }}
118                    }}
119                    "#
120            )
121        }))
122        .collect::<String>();
123    output.push(CodeFile {
124        filename: "lib.rs".to_owned(),
125        code: utils::fmt(&code)?,
126        dir: String::new(),
127    });
128
129    Ok(output)
130}
131
132/// Generate a single element.
133fn generate_element(el: MergedElement) -> Result<CodeFile> {
134    let dir = el.submodule_name.clone();
135    let MergedElement {
136        tag_name,
137        struct_name,
138        has_closing_tag,
139        attributes,
140        mdn_link,
141        has_global_attributes,
142        ..
143    } = el;
144
145    let filename = format!("{}.rs", tag_name);
146    let fields = generate_fields(&attributes);
147    let opening_tag_content = generate_opening_tag(&attributes, &tag_name, has_global_attributes);
148    let closing_tag_content = generate_closing_tag(&tag_name, has_closing_tag);
149
150    let global_field = match has_global_attributes {
151        true => format!("global_attrs: crate::GlobalAttributes,"),
152        false => String::new(),
153    };
154
155    let mut code = formatdoc!(
156        r#"/// The HTML `<{tag_name}>` element
157        ///
158        /// [MDN Documentation]({mdn_link})
159        #[doc(alias = "{tag_name}")]
160        #[non_exhaustive]
161        #[derive(Debug, Clone, PartialEq, Default)]
162        pub struct {struct_name} {{
163            pub data_map: crate::DataMap,
164            {global_field}
165            {fields}
166        }}
167
168        impl crate::RenderElement for {struct_name} {{
169            fn write_opening_tag<W: std::fmt::Write>(&self, writer: &mut W) -> std::fmt::Result {{
170                {opening_tag_content}
171                Ok(())
172            }}
173
174            #[allow(unused_variables)]
175            fn write_closing_tag<W: std::fmt::Write>(&self, writer: &mut W) -> std::fmt::Result {{
176                {closing_tag_content}
177                Ok(())
178            }}
179        }}
180
181        impl std::fmt::Display for {struct_name} {{
182            fn fmt(&self, writer: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{
183                use crate::RenderElement;
184                self.write_opening_tag(writer)?;
185                self.write_closing_tag(writer)?;
186                Ok(())
187            }}
188        }}
189    "#
190    );
191
192    if has_global_attributes {
193        code.push_str(&formatdoc!(
194            r#"
195            impl std::ops::Deref for {struct_name} {{
196                type Target = crate::GlobalAttributes;
197
198                fn deref(&self) -> &Self::Target {{
199                    &self.global_attrs
200                }}
201            }}
202
203            impl std::ops::DerefMut for {struct_name} {{
204                fn deref_mut(&mut self) -> &mut Self::Target {{
205                    &mut self.global_attrs
206                }}
207            }}"#
208        ));
209    }
210
211    Ok(CodeFile {
212        filename,
213        code: utils::fmt(&code)?,
214        dir,
215    })
216}
217
218fn generate_fields(attributes: &[Attribute]) -> String {
219    let mut output = String::new();
220    for attr in attributes {
221        let description = &attr.description;
222        let field_name = &attr.field_name;
223        let ty = &attr.ty;
224        output.push_str(&match ty {
225            AttributeType::Bool => format!(
226                "/// {description}
227                pub {field_name}: bool,
228                "
229            ),
230            AttributeType::String => format!(
231                "/// {description}
232             pub {field_name}: std::option::Option<std::borrow::Cow<'static, str>>,
233            "
234            ),
235            _ => format!(
236                "/// {description}
237             pub {field_name}: std::option::Option<{ty}>,
238            "
239            ),
240        });
241    }
242    output
243}
244
245fn generate_opening_tag(
246    attributes: &[Attribute],
247    tag_name: &str,
248    has_global_attrs: bool,
249) -> String {
250    let preamble = match tag_name {
251        "html" => "<!DOCTYPE html>",
252        _ => "",
253    };
254    let mut output = formatdoc!(
255        r#"
256        write!(writer, "{preamble}<{tag_name}")?;
257    "#
258    );
259    for attr in attributes {
260        output.push_str(&generate_attribute_display(&attr));
261    }
262    if has_global_attrs {
263        output.push_str(&format!(r#"write!(writer, "{{}}", self.global_attrs)?;"#));
264    }
265
266    output.push_str(&format!(r#"write!(writer, "{{}}", self.data_map)?;"#));
267    writedoc!(&mut output, r#"write!(writer, ">")?;"#).unwrap();
268    output
269}
270
271fn generate_closing_tag(tag_name: &str, has_closing_tag: bool) -> String {
272    if has_closing_tag {
273        formatdoc!(
274            r#"write!(writer, "</{tag_name}>")?;
275        "#
276        )
277    } else {
278        String::new()
279    }
280}
281
282fn generate_attribute_display(attr: &Attribute) -> String {
283    let Attribute {
284        name,
285        field_name,
286        ty,
287        ..
288    } = &attr;
289    match ty {
290        AttributeType::Bool => format!(
291            r##"if self.{field_name} {{
292                    write!(writer, r#" {name}"#)?;
293            }}"##
294        ),
295        AttributeType::String | AttributeType::Integer | AttributeType::Float => format!(
296            r##"if let Some(field) = self.{field_name}.as_ref() {{
297                write!(writer, r#" {name}="{{field}}""#)?;
298            }}"##
299        ),
300        AttributeType::Identifier(_) => todo!(),
301        AttributeType::Enumerable(_) => todo!(),
302    }
303}