1use std::io::{self, Write};
5
6use crate::core::node::write_document_root;
7use crate::core::{Attribute, Element, Node, Render, RenderOptions};
8use crate::elements::{body, head, html_tag};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
33#[non_exhaustive]
34pub struct Document {
35 pub lang: Option<String>,
37 pub head: Element,
39 pub body: Element,
41}
42
43impl Default for Document {
44 fn default() -> Self {
46 Self::new(None)
47 }
48}
49
50impl std::fmt::Display for Document {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 f.write_str(&self.render())
54 }
55}
56
57impl Document {
58 #[must_use]
60 pub fn new(lang: Option<&str>) -> Self {
61 Self {
62 lang: lang.map(ToString::to_string),
63 head: head(),
64 body: body(),
65 }
66 }
67
68 #[must_use]
70 pub fn with_parts(lang: Option<&str>, head: Element, body: Element) -> Self {
71 Self {
72 lang: lang.map(ToString::to_string),
73 head,
74 body,
75 }
76 }
77
78 #[must_use]
80 pub fn head_children<N: Into<Node>>(mut self, children: impl IntoIterator<Item = N>) -> Self {
81 self.head = self.head.children_from(children);
82 self
83 }
84
85 #[must_use]
87 pub fn body_children<N: Into<Node>>(mut self, children: impl IntoIterator<Item = N>) -> Self {
88 self.body = self.body.children_from(children);
89 self
90 }
91
92 #[must_use]
97 pub fn root(&self) -> Element {
98 let root = match &self.lang {
99 Some(lang) => html_tag().attr("lang", lang),
100 None => html_tag(),
101 };
102 root.child(self.head.clone()).child(self.body.clone())
103 }
104
105 #[must_use]
107 pub fn render_with(&self, options: &RenderOptions) -> String {
108 let mut out = String::with_capacity(2048);
109 out.push_str("<!DOCTYPE html>\n");
110 write_document_root(
113 self.lang.as_deref(),
114 &self.head,
115 &self.body,
116 &mut out,
117 options,
118 );
119 out
120 }
121
122 #[must_use]
124 pub fn render(&self) -> String {
125 self.render_with(&RenderOptions::pretty())
126 }
127
128 #[must_use]
130 pub fn render_compact(&self) -> String {
131 self.render_with(&RenderOptions::compact())
132 }
133
134 pub fn render_to(&self, mut sink: impl Write, options: &RenderOptions) -> io::Result<()> {
156 if options.max_depth.is_some() || !streams_by_section(&self.body) {
159 return sink.write_all(self.render_with(options).as_bytes());
160 }
161 sink.write_all(self.html_open().as_bytes())?;
162 if options.pretty {
163 self.write_pretty_sections(&mut sink, options)
164 } else {
165 self.write_compact_sections(&mut sink, options)
166 }
167 }
168
169 fn html_open(&self) -> String {
170 let mut open = String::from("<!DOCTYPE html>\n<html");
171 if let Some(lang) = &self.lang {
172 Attribute::new_static("lang", lang).write_into(&mut open);
173 }
174 open.push('>');
175 open
176 }
177
178 fn write_pretty_sections(
179 &self,
180 sink: &mut impl Write,
181 options: &RenderOptions,
182 ) -> io::Result<()> {
183 let mut head_markup = String::from("\n");
184 self.head.write_into(&mut head_markup, options, 1);
185 sink.write_all(head_markup.as_bytes())?;
186
187 let mut body_open = String::from("\n");
188 options.write_indent(&mut body_open, 1);
189 write_open_tag(&self.body, &mut body_open);
190 sink.write_all(body_open.as_bytes())?;
191
192 for child in self.body.children() {
193 let mut rendered = String::new();
194 child.write_into(&mut rendered, options, 2);
195 if !rendered.is_empty() {
196 sink.write_all(b"\n")?;
197 sink.write_all(rendered.as_bytes())?;
198 }
199 }
200
201 let mut close = String::from("\n");
202 options.write_indent(&mut close, 1);
203 close.push_str("</body>\n</html>");
204 sink.write_all(close.as_bytes())
205 }
206
207 fn write_compact_sections(
208 &self,
209 sink: &mut impl Write,
210 options: &RenderOptions,
211 ) -> io::Result<()> {
212 let mut head_markup = String::new();
213 self.head.write_into(&mut head_markup, options, 0);
214 sink.write_all(head_markup.as_bytes())?;
215
216 let mut body_open = String::new();
217 write_open_tag(&self.body, &mut body_open);
218 sink.write_all(body_open.as_bytes())?;
219
220 for child in self.body.children() {
221 let mut rendered = String::new();
222 child.write_into(&mut rendered, options, 0);
223 sink.write_all(rendered.as_bytes())?;
224 }
225 sink.write_all(b"</body></html>")
226 }
227}
228
229fn streams_by_section(body: &Element) -> bool {
232 body.tag() == "body"
233 && body.content().is_none()
234 && !body.children().is_empty()
235 && !body.is_self_closing()
236}
237
238fn write_open_tag(element: &Element, out: &mut String) {
239 out.push('<');
240 out.push_str(element.tag());
241 for attribute in element.attributes() {
242 attribute.write_into(out);
243 }
244 out.push('>');
245}
246
247#[cfg(test)]
248mod tests {
249 #[test]
252 fn a_depth_limit_gives_the_same_bytes_streamed_and_whole() {
253 use crate::elements::{div, p, title};
254 let page = Document::new(Some("en"))
255 .head_children([title().text("T")])
256 .body_children([
257 div().child(div().child(p().text("deep"))),
258 div().text("flat"),
259 ]);
260 for depth in 0..6 {
261 for base in [RenderOptions::pretty(), RenderOptions::compact()] {
262 let options = base.with_max_depth(depth);
263 let mut streamed = Vec::new();
264 page.render_to(&mut streamed, &options)
265 .expect("a Vec never fails");
266 assert_eq!(
267 String::from_utf8(streamed).expect("utf-8"),
268 page.render_with(&options),
269 "depth {depth}"
270 );
271 }
272 }
273 let rendered = page.render_with(&RenderOptions::compact().with_max_depth(2));
275 assert_eq!(
276 rendered,
277 "<!DOCTYPE html>\n<html lang=\"en\"><head></head><body></body></html>"
278 );
279 }
280
281 use super::*;
282 use crate::core::Node;
283 use crate::elements::{body, h1, h2, head, img, meta, p, section, title};
284
285 #[test]
287 fn rendering_matches_the_cloned_root_tree_byte_for_byte() {
288 use crate::elements::{div, p, pre, title};
289 let documents = [
290 Document::new(None),
291 Document::new(Some("pt-BR")).head_children([title().text("T")]),
292 Document::new(Some("a\"b")).body_children([div().child(p().text("x")), div()]),
293 Document::new(Some("en")).body_children([Node::fragment([]), Node::text("")]),
294 Document::with_parts(Some("en"), head(), pre().text(" keep ")),
295 Document::with_parts(None, Element::new("bad tag"), body().text("t")),
296 Document::with_parts(None, head(), body().self_closing(true)),
297 ];
298 for document in &documents {
299 for options in [
300 RenderOptions::pretty(),
301 RenderOptions::compact(),
302 RenderOptions::pretty().with_xhtml_self_closing(true),
303 RenderOptions::compact().with_indent("\t"),
304 ] {
305 let mut expected = String::from("<!DOCTYPE html>\n");
306 document.root().write_into(&mut expected, &options, 0);
307 assert_eq!(document.render_with(&options), expected, "{document:?}");
308 }
309 }
310 }
311
312 #[test]
313 fn a_document_owns_the_doctype_and_the_lang_attribute() {
314 let page = Document::new(Some("pt-BR"));
315 assert!(
316 page.render()
317 .starts_with("<!DOCTYPE html>\n<html lang=\"pt-BR\">")
318 );
319 }
320
321 #[test]
322 fn the_lang_attribute_is_omitted_when_absent() {
323 let page = Document::new(None);
324 assert!(page.render().starts_with("<!DOCTYPE html>\n<html>"));
325 }
326
327 #[test]
328 fn root_returns_the_tree_without_the_doctype() {
329 let page = Document::new(Some("en"));
330 assert!(!page.root().render().contains("DOCTYPE"));
331 assert!(page.root().render().starts_with("<html lang=\"en\">"));
332 }
333
334 #[test]
336 fn a_document_renders_pretty_by_default_unlike_an_element() {
337 let page = Document::new(Some("en")).head_children([title().text("T")]);
338 assert!(page.render().contains('\n'));
339 assert!(
340 !page
341 .render_compact()
342 .trim_start_matches("<!DOCTYPE html>\n")
343 .contains('\n')
344 );
345 }
346
347 #[test]
348 fn head_and_body_children_land_in_the_right_place() {
349 let page = Document::new(Some("en"))
350 .head_children([title().text("T")])
351 .body_children([h1().text("H")]);
352 let rendered = page.render();
353 let head_at = rendered.find("<title>").expect("title rendered");
354 let body_at = rendered.find("<h1>").expect("h1 rendered");
355 assert!(head_at < body_at);
356 }
357
358 #[test]
359 fn a_document_is_a_value_and_can_be_shared_across_threads() {
360 fn assert_send_sync<T: Send + Sync>() {}
361 assert_send_sync::<Document>();
362
363 let page = Document::new(Some("en"));
364 let copy = page.clone().head_children([title().text("changed")]);
365 assert!(!page.render().contains("changed"));
366 assert!(copy.render().contains("changed"));
367 }
368
369 #[test]
370 fn body_children_accept_loops_and_conditions() {
371 let show_banner = false;
372 let page = Document::new(None)
373 .head_children([title().text("T")])
374 .body_children([crate::html! {
375 @if show_banner { div { "banner" } }
376 @for index in 1..=2 { p { "line " (index) } }
377 }]);
378
379 let rendered = page.render_compact();
380 assert!(!rendered.contains("banner"));
381 assert!(rendered.contains("<p>line 1</p><p>line 2</p>"));
382 }
383
384 #[test]
386 fn render_to_matches_render_with() {
387 let options = [
388 RenderOptions::pretty(),
389 RenderOptions::compact(),
390 RenderOptions::pretty()
391 .with_indent("\t")
392 .with_xhtml_self_closing(true),
393 ];
394 let documents = [
395 streaming_page(3, false),
396 streaming_page(400, false),
397 streaming_page(2, true),
398 Document::with_parts(None, head(), body()),
399 Document::with_parts(Some("en"), head(), body().raw_text("<raw>")),
400 ];
401 for page in &documents {
402 for options in &options {
403 let mut written = Vec::new();
404 page.render_to(&mut written, options).expect("write");
405 assert_eq!(written, page.render_with(options).into_bytes());
406 }
407 }
408 }
409
410 fn streaming_page(sections: usize, empty: bool) -> Document {
411 let mut nodes = Vec::new();
412 if empty {
413 nodes.push(Node::fragment([]));
414 }
415 for index in 0..sections {
416 nodes.push(
417 section()
418 .add_class("s")
419 .child(h2().text(format!("Section {index}")))
420 .child(p().text("text & more ".repeat(50)))
421 .child(img().attr("src", format!("/{index}.png")).attr("alt", ""))
422 .into(),
423 );
424 }
425 if empty {
426 nodes.push(Node::fragment([]));
427 }
428 Document::new(Some("en"))
429 .head_children([title().text("Streaming"), meta().attr("charset", "UTF-8")])
430 .body_children(nodes)
431 }
432}