1use std::io::{self, Write};
5
6use crate::core::node::{write_document_root, write_html_open_tag};
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 html_attributes: Vec<Attribute>,
40 pub head: Element,
42 pub body: Element,
44}
45
46impl Default for Document {
47 fn default() -> Self {
49 Self::new(None)
50 }
51}
52
53impl std::fmt::Display for Document {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 f.write_str(&self.render())
57 }
58}
59
60impl Document {
61 #[must_use]
63 pub fn new(lang: Option<&str>) -> Self {
64 Self {
65 lang: lang.map(ToString::to_string),
66 html_attributes: Vec::new(),
67 head: head(),
68 body: body(),
69 }
70 }
71
72 #[must_use]
74 pub fn with_parts(lang: Option<&str>, head: Element, body: Element) -> Self {
75 Self {
76 lang: lang.map(ToString::to_string),
77 html_attributes: Vec::new(),
78 head,
79 body,
80 }
81 }
82
83 #[must_use]
97 pub fn html_attr(mut self, key: impl Into<String>, value: impl AsRef<str>) -> Self {
98 self.html_attributes.push(Attribute::new(key, value));
99 self
100 }
101
102 #[must_use]
111 pub fn html_attrs<K: Into<String>, V: AsRef<str>>(
112 mut self,
113 attributes: impl IntoIterator<Item = (K, V)>,
114 ) -> Self {
115 self.html_attributes.extend(
116 attributes
117 .into_iter()
118 .map(|(key, value)| Attribute::new(key, value)),
119 );
120 self
121 }
122
123 #[must_use]
125 pub fn head_children<N: Into<Node>>(mut self, children: impl IntoIterator<Item = N>) -> Self {
126 self.head = self.head.children_from(children);
127 self
128 }
129
130 #[must_use]
132 pub fn body_children<N: Into<Node>>(mut self, children: impl IntoIterator<Item = N>) -> Self {
133 self.body = self.body.children_from(children);
134 self
135 }
136
137 #[must_use]
142 pub fn root(&self) -> Element {
143 let mut root = match &self.lang {
144 Some(lang) => html_tag().attr("lang", lang),
145 None => html_tag(),
146 };
147 for attribute in &self.html_attributes {
148 root = root.add_attribute(attribute.clone());
149 }
150 root.child(self.head.clone()).child(self.body.clone())
151 }
152
153 #[must_use]
155 pub fn render_with(&self, options: &RenderOptions) -> String {
156 let mut out = String::with_capacity(2048);
157 out.push_str("<!DOCTYPE html>\n");
158 write_document_root(
161 self.lang.as_deref(),
162 &self.html_attributes,
163 &self.head,
164 &self.body,
165 &mut out,
166 options,
167 );
168 out
169 }
170
171 #[must_use]
173 pub fn render(&self) -> String {
174 self.render_with(&RenderOptions::pretty())
175 }
176
177 #[must_use]
179 pub fn render_compact(&self) -> String {
180 self.render_with(&RenderOptions::compact())
181 }
182
183 pub fn render_to(&self, mut sink: impl Write, options: &RenderOptions) -> io::Result<()> {
205 if options.max_depth.is_some() || !streams_by_section(&self.body) {
208 return sink.write_all(self.render_with(options).as_bytes());
209 }
210 sink.write_all(self.html_open().as_bytes())?;
211 if options.pretty {
212 self.write_pretty_sections(&mut sink, options)
213 } else {
214 self.write_compact_sections(&mut sink, options)
215 }
216 }
217
218 fn html_open(&self) -> String {
219 let mut open = String::from("<!DOCTYPE html>\n");
220 write_html_open_tag(self.lang.as_deref(), &self.html_attributes, &mut open);
221 open
222 }
223
224 fn write_pretty_sections(
225 &self,
226 sink: &mut impl Write,
227 options: &RenderOptions,
228 ) -> io::Result<()> {
229 let mut head_markup = String::from("\n");
230 self.head.write_into(&mut head_markup, options, 1);
231 sink.write_all(head_markup.as_bytes())?;
232
233 let mut body_open = String::from("\n");
234 options.write_indent(&mut body_open, 1);
235 write_open_tag(&self.body, &mut body_open);
236 sink.write_all(body_open.as_bytes())?;
237
238 for child in self.body.children() {
239 let mut rendered = String::new();
240 child.write_into(&mut rendered, options, 2);
241 if !rendered.is_empty() {
242 sink.write_all(b"\n")?;
243 sink.write_all(rendered.as_bytes())?;
244 }
245 }
246
247 let mut close = String::from("\n");
248 options.write_indent(&mut close, 1);
249 close.push_str("</body>\n</html>");
250 sink.write_all(close.as_bytes())
251 }
252
253 fn write_compact_sections(
254 &self,
255 sink: &mut impl Write,
256 options: &RenderOptions,
257 ) -> io::Result<()> {
258 let mut head_markup = String::new();
259 self.head.write_into(&mut head_markup, options, 0);
260 sink.write_all(head_markup.as_bytes())?;
261
262 let mut body_open = String::new();
263 write_open_tag(&self.body, &mut body_open);
264 sink.write_all(body_open.as_bytes())?;
265
266 for child in self.body.children() {
267 let mut rendered = String::new();
268 child.write_into(&mut rendered, options, 0);
269 sink.write_all(rendered.as_bytes())?;
270 }
271 sink.write_all(b"</body></html>")
272 }
273}
274
275fn streams_by_section(body: &Element) -> bool {
278 body.tag() == "body"
279 && body.content().is_none()
280 && !body.children().is_empty()
281 && !body.is_self_closing()
282}
283
284fn write_open_tag(element: &Element, out: &mut String) {
285 out.push('<');
286 out.push_str(element.tag());
287 for attribute in element.attributes() {
288 attribute.write_into(out);
289 }
290 out.push('>');
291}
292
293#[cfg(test)]
294mod tests {
295 #[test]
298 fn a_depth_limit_gives_the_same_bytes_streamed_and_whole() {
299 use crate::elements::{div, p, title};
300 let page = Document::new(Some("en"))
301 .head_children([title().text("T")])
302 .body_children([
303 div().child(div().child(p().text("deep"))),
304 div().text("flat"),
305 ]);
306 for depth in 0..6 {
307 for base in [RenderOptions::pretty(), RenderOptions::compact()] {
308 let options = base.with_max_depth(depth);
309 let mut streamed = Vec::new();
310 page.render_to(&mut streamed, &options)
311 .expect("a Vec never fails");
312 assert_eq!(
313 String::from_utf8(streamed).expect("utf-8"),
314 page.render_with(&options),
315 "depth {depth}"
316 );
317 }
318 }
319 let rendered = page.render_with(&RenderOptions::compact().with_max_depth(2));
321 assert_eq!(
322 rendered,
323 "<!DOCTYPE html>\n<html lang=\"en\"><head></head><body></body></html>"
324 );
325 }
326
327 use super::*;
328 use crate::core::Node;
329 use crate::elements::{body, h1, h2, head, img, meta, p, section, title};
330
331 #[test]
333 fn rendering_matches_the_cloned_root_tree_byte_for_byte() {
334 use crate::elements::{div, p, pre, title};
335 let documents = [
336 Document::new(None),
337 Document::new(Some("pt-BR")).head_children([title().text("T")]),
338 Document::new(Some("a\"b")).body_children([div().child(p().text("x")), div()]),
339 Document::new(Some("en")).body_children([Node::fragment([]), Node::text("")]),
340 Document::with_parts(Some("en"), head(), pre().text(" keep ")),
341 Document::with_parts(None, Element::new("bad tag"), body().text("t")),
342 Document::with_parts(None, head(), body().self_closing(true)),
343 Document::new(Some("en"))
344 .html_attr("data-theme", "dark")
345 .html_attr("class", "a\"b")
346 .body_children([div().child(p().text("x"))]),
347 Document::new(None).html_attr("on load", "x"),
348 ];
349 for document in &documents {
350 for options in [
351 RenderOptions::pretty(),
352 RenderOptions::compact(),
353 RenderOptions::pretty().with_xhtml_self_closing(true),
354 RenderOptions::compact().with_indent("\t"),
355 ] {
356 let mut expected = String::from("<!DOCTYPE html>\n");
357 document.root().write_into(&mut expected, &options, 0);
358 assert_eq!(document.render_with(&options), expected, "{document:?}");
359 }
360 }
361 }
362
363 #[test]
364 fn html_attributes_follow_lang_in_every_render_path() {
365 use crate::elements::h1;
366 let page = Document::new(Some("pt-BR"))
367 .html_attr("data-theme", "dark")
368 .html_attrs([("dir", "ltr")])
369 .body_children([h1().text("Hi")]);
370 let open = "<html lang=\"pt-BR\" data-theme=\"dark\" dir=\"ltr\">";
371
372 assert_eq!(
373 page.render_compact(),
374 format!("<!DOCTYPE html>\n{open}<head></head><body><h1>Hi</h1></body></html>")
375 );
376 assert_eq!(
377 page.render(),
378 format!(
379 "<!DOCTYPE html>\n{open}\n <head></head>\n <body>\n <h1>Hi</h1>\n </body>\n</html>"
380 )
381 );
382 assert_eq!(
383 page.root().render(),
384 format!("{open}<head></head><body><h1>Hi</h1></body></html>")
385 );
386 for options in [RenderOptions::pretty(), RenderOptions::compact()] {
387 let mut streamed = Vec::new();
388 page.render_to(&mut streamed, &options).unwrap();
389 assert_eq!(
390 String::from_utf8(streamed).unwrap(),
391 page.render_with(&options)
392 );
393 }
394 }
395
396 #[test]
397 fn an_unsafe_html_attribute_name_is_omitted_and_a_value_is_escaped() {
398 let page = Document::new(None)
399 .html_attr("on load=alert(1) x", "1")
400 .html_attr("data-x", "\"><script>");
401 assert!(
402 page.render_compact()
403 .starts_with("<!DOCTYPE html>\n<html data-x=\""><script>\">"),
404 "{}",
405 page.render_compact()
406 );
407 }
408
409 #[test]
410 fn a_document_without_html_attributes_is_unchanged() {
411 assert_eq!(
412 Document::new(Some("en")).render_compact(),
413 "<!DOCTYPE html>\n<html lang=\"en\"><head></head><body></body></html>"
414 );
415 }
416
417 #[test]
418 fn a_document_owns_the_doctype_and_the_lang_attribute() {
419 let page = Document::new(Some("pt-BR"));
420 assert!(
421 page.render()
422 .starts_with("<!DOCTYPE html>\n<html lang=\"pt-BR\">")
423 );
424 }
425
426 #[test]
427 fn the_lang_attribute_is_omitted_when_absent() {
428 let page = Document::new(None);
429 assert!(page.render().starts_with("<!DOCTYPE html>\n<html>"));
430 }
431
432 #[test]
433 fn root_returns_the_tree_without_the_doctype() {
434 let page = Document::new(Some("en"));
435 assert!(!page.root().render().contains("DOCTYPE"));
436 assert!(page.root().render().starts_with("<html lang=\"en\">"));
437 }
438
439 #[test]
441 fn a_document_renders_pretty_by_default_unlike_an_element() {
442 let page = Document::new(Some("en")).head_children([title().text("T")]);
443 assert!(page.render().contains('\n'));
444 assert!(
445 !page
446 .render_compact()
447 .trim_start_matches("<!DOCTYPE html>\n")
448 .contains('\n')
449 );
450 }
451
452 #[test]
453 fn head_and_body_children_land_in_the_right_place() {
454 let page = Document::new(Some("en"))
455 .head_children([title().text("T")])
456 .body_children([h1().text("H")]);
457 let rendered = page.render();
458 let head_at = rendered.find("<title>").expect("title rendered");
459 let body_at = rendered.find("<h1>").expect("h1 rendered");
460 assert!(head_at < body_at);
461 }
462
463 #[test]
464 fn a_document_is_a_value_and_can_be_shared_across_threads() {
465 fn assert_send_sync<T: Send + Sync>() {}
466 assert_send_sync::<Document>();
467
468 let page = Document::new(Some("en"));
469 let copy = page.clone().head_children([title().text("changed")]);
470 assert!(!page.render().contains("changed"));
471 assert!(copy.render().contains("changed"));
472 }
473
474 #[test]
475 fn body_children_accept_loops_and_conditions() {
476 let show_banner = false;
477 let page = Document::new(None)
478 .head_children([title().text("T")])
479 .body_children([crate::html! {
480 @if show_banner { div { "banner" } }
481 @for index in 1..=2 { p { "line " (index) } }
482 }]);
483
484 let rendered = page.render_compact();
485 assert!(!rendered.contains("banner"));
486 assert!(rendered.contains("<p>line 1</p><p>line 2</p>"));
487 }
488
489 #[test]
491 fn render_to_matches_render_with() {
492 let options = [
493 RenderOptions::pretty(),
494 RenderOptions::compact(),
495 RenderOptions::pretty()
496 .with_indent("\t")
497 .with_xhtml_self_closing(true),
498 ];
499 let documents = [
500 streaming_page(3, false),
501 streaming_page(400, false),
502 streaming_page(2, true),
503 Document::with_parts(None, head(), body()),
504 Document::with_parts(Some("en"), head(), body().raw_text("<raw>")),
505 ];
506 for page in &documents {
507 for options in &options {
508 let mut written = Vec::new();
509 page.render_to(&mut written, options).expect("write");
510 assert_eq!(written, page.render_with(options).into_bytes());
511 }
512 }
513 }
514
515 fn streaming_page(sections: usize, empty: bool) -> Document {
516 let mut nodes = Vec::new();
517 if empty {
518 nodes.push(Node::fragment([]));
519 }
520 for index in 0..sections {
521 nodes.push(
522 section()
523 .add_class("s")
524 .child(h2().text(format!("Section {index}")))
525 .child(p().text("text & more ".repeat(50)))
526 .child(img().attr("src", format!("/{index}.png")).attr("alt", ""))
527 .into(),
528 );
529 }
530 if empty {
531 nodes.push(Node::fragment([]));
532 }
533 Document::new(Some("en"))
534 .head_children([title().text("Streaming"), meta().attr("charset", "UTF-8")])
535 .body_children(nodes)
536 }
537}