html_bindgen/generate/sys/
mod.rs1use 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 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 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 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
132fn 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}