swc_html_codegen/lib.rs
1#![deny(clippy::all)]
2#![allow(clippy::needless_update)]
3#![allow(clippy::match_like_matches_macro)]
4#![allow(non_local_definitions)]
5
6pub use std::fmt::Result;
7use std::{borrow::Cow, iter::Peekable, str::Chars};
8
9use swc_atoms::Atom;
10use swc_common::Spanned;
11use swc_html_ast::*;
12use swc_html_codegen_macros::emitter;
13use swc_html_utils::HTML_ENTITIES;
14use writer::HtmlWriter;
15
16pub use self::emit::*;
17use self::{ctx::Ctx, list::ListFormat};
18
19#[macro_use]
20mod macros;
21mod ctx;
22mod emit;
23mod list;
24pub mod writer;
25
26#[derive(Debug, Clone, Default)]
27pub struct CodegenConfig<'a> {
28 pub minify: bool,
29 pub scripting_enabled: bool,
30 /// Should be used only for `DocumentFragment` code generation
31 pub context_element: Option<&'a Element>,
32 /// Don't print optional tags (only when `minify` enabled)
33 /// By default `true` when `minify` enabled, otherwise `false`
34 pub tag_omission: Option<bool>,
35 /// Keep <head> tags and </body> closing tag when `tag_omission` is enabled
36 pub keep_head_and_body: Option<bool>,
37 /// Making SVG and MathML elements self-closing where possible (only when
38 /// `minify` enabled) By default `false` when `minify` enabled,
39 /// otherwise `true`
40 pub self_closing_void_elements: Option<bool>,
41 /// Always print quotes or remove them where possible (only when `minify`
42 /// enabled) By default `false` when `minify` enabled, otherwise `true`
43 pub quotes: Option<bool>,
44}
45
46enum TagOmissionParent<'a> {
47 Document(&'a Document),
48 DocumentFragment(&'a DocumentFragment),
49 Element(&'a Element),
50}
51
52#[derive(Debug)]
53pub struct CodeGenerator<'a, W>
54where
55 W: HtmlWriter,
56{
57 wr: W,
58 config: CodegenConfig<'a>,
59 ctx: Ctx,
60 // For legacy `<plaintext>`
61 is_plaintext: bool,
62 tag_omission: bool,
63 keep_head_and_body: bool,
64 self_closing_void_elements: bool,
65 quotes: bool,
66}
67
68impl<'a, W> CodeGenerator<'a, W>
69where
70 W: HtmlWriter,
71{
72 pub fn new(wr: W, config: CodegenConfig<'a>) -> Self {
73 let tag_omission = config.tag_omission.unwrap_or(config.minify);
74 let keep_head_and_body = config.keep_head_and_body.unwrap_or(false);
75 let self_closing_void_elements = config.tag_omission.unwrap_or(!config.minify);
76 let quotes = config.quotes.unwrap_or(!config.minify);
77
78 CodeGenerator {
79 wr,
80 config,
81 ctx: Default::default(),
82 is_plaintext: false,
83 tag_omission,
84 keep_head_and_body,
85 self_closing_void_elements,
86 quotes,
87 }
88 }
89
90 #[emitter]
91 fn emit_document(&mut self, n: &Document) -> Result {
92 if self.tag_omission {
93 self.emit_list_for_tag_omission(TagOmissionParent::Document(n))?;
94 } else {
95 self.emit_list(&n.children, ListFormat::NotDelimited)?;
96 }
97 }
98
99 #[emitter]
100 fn emit_document_fragment(&mut self, n: &DocumentFragment) -> Result {
101 let ctx = if let Some(context_element) = &self.config.context_element {
102 self.create_context_for_element(context_element)
103 } else {
104 Default::default()
105 };
106
107 if self.tag_omission {
108 self.with_ctx(ctx)
109 .emit_list_for_tag_omission(TagOmissionParent::DocumentFragment(n))?;
110 } else {
111 self.with_ctx(ctx)
112 .emit_list(&n.children, ListFormat::NotDelimited)?;
113 }
114 }
115
116 #[emitter]
117 fn emit_child(&mut self, n: &Child) -> Result {
118 match n {
119 Child::DocumentType(n) => emit!(self, n),
120 Child::Element(n) => emit!(self, n),
121 Child::Text(n) => emit!(self, n),
122 Child::Comment(n) => emit!(self, n),
123 }
124 }
125
126 #[emitter]
127 fn emit_document_doctype(&mut self, n: &DocumentType) -> Result {
128 let mut doctype = String::with_capacity(
129 10 + if let Some(name) = &n.name {
130 name.len() + 1
131 } else {
132 0
133 } + if let Some(public_id) = &n.public_id {
134 let mut len = public_id.len() + 10;
135
136 if let Some(system_id) = &n.system_id {
137 len += system_id.len() + 3
138 }
139
140 len
141 } else if let Some(system_id) = &n.system_id {
142 system_id.len() + 10
143 } else {
144 0
145 },
146 );
147
148 doctype.push('<');
149 doctype.push('!');
150
151 if self.config.minify {
152 doctype.push_str("doctype");
153 } else {
154 doctype.push_str("DOCTYPE");
155 }
156
157 if let Some(name) = &n.name {
158 doctype.push(' ');
159 doctype.push_str(name);
160 }
161
162 if let Some(public_id) = &n.public_id {
163 doctype.push(' ');
164
165 if self.config.minify {
166 doctype.push_str("public");
167 } else {
168 doctype.push_str("PUBLIC");
169 }
170
171 doctype.push(' ');
172
173 let public_id_quote = if public_id.contains('"') { '\'' } else { '"' };
174
175 doctype.push(public_id_quote);
176 doctype.push_str(public_id);
177 doctype.push(public_id_quote);
178
179 if let Some(system_id) = &n.system_id {
180 doctype.push(' ');
181
182 let system_id_quote = if system_id.contains('"') { '\'' } else { '"' };
183
184 doctype.push(system_id_quote);
185 doctype.push_str(system_id);
186 doctype.push(system_id_quote);
187 }
188 } else if let Some(system_id) = &n.system_id {
189 doctype.push(' ');
190
191 if self.config.minify {
192 doctype.push_str("system");
193 } else {
194 doctype.push_str("SYSTEM");
195 }
196
197 doctype.push(' ');
198
199 let system_id_quote = if system_id.contains('"') { '\'' } else { '"' };
200
201 doctype.push(system_id_quote);
202 doctype.push_str(system_id);
203 doctype.push(system_id_quote);
204 }
205
206 doctype.push('>');
207
208 write_multiline_raw!(self, n.span, &doctype);
209 formatting_newline!(self);
210 }
211
212 fn basic_emit_element(
213 &mut self,
214 n: &Element,
215 parent: Option<&Element>,
216 prev: Option<&Child>,
217 next: Option<&Child>,
218 ) -> Result {
219 if self.is_plaintext {
220 return Ok(());
221 }
222
223 let has_attributes = !n.attributes.is_empty();
224 let can_omit_start_tag = self.tag_omission
225 && !has_attributes
226 && n.namespace == Namespace::HTML
227 && match &*n.tag_name {
228 // Tag omission in text/html:
229 // An html element's start tag can be omitted if the first thing inside the html
230 // element is not a comment.
231 "html" if !matches!(n.children.first(), Some(Child::Comment(..))) => true,
232 // A head element's start tag can be omitted if the element is empty, or if the
233 // first thing inside the head element is an element.
234 "head"
235 if !self.keep_head_and_body
236 && (n.children.is_empty()
237 || matches!(n.children.first(), Some(Child::Element(..)))) =>
238 {
239 true
240 }
241 // A body element's start tag can be omitted if the element is empty, or if the
242 // first thing inside the body element is not ASCII whitespace or a comment, except
243 // if the first thing inside the body element would be parsed differently outside.
244 "body"
245 if !self.keep_head_and_body
246 && (n.children.is_empty()
247 || (match n.children.first() {
248 Some(Child::Text(text))
249 if !text.data.is_empty()
250 && text
251 .data
252 .chars()
253 .next()
254 .unwrap()
255 .is_ascii_whitespace() =>
256 {
257 false
258 }
259 Some(Child::Comment(..)) => false,
260 Some(Child::Element(Element {
261 namespace,
262 tag_name,
263 ..
264 })) if *namespace == Namespace::HTML
265 && matches!(
266 &**tag_name,
267 "base"
268 | "basefont"
269 | "bgsound"
270 | "frameset"
271 | "link"
272 | "meta"
273 | "noframes"
274 | "noscript"
275 | "script"
276 | "style"
277 | "template"
278 | "title"
279 ) =>
280 {
281 false
282 }
283 _ => true,
284 })) =>
285 {
286 true
287 }
288 // A colgroup element's start tag can be omitted if the first thing inside the
289 // colgroup element is a col element, and if the element is not immediately preceded
290 // by another colgroup element whose end tag has been omitted. (It can't be omitted
291 // if the element is empty.)
292 "colgroup"
293 if match n.children.first() {
294 Some(Child::Element(element))
295 if element.namespace == Namespace::HTML
296 && element.tag_name == "col" =>
297 {
298 !matches!(prev, Some(Child::Element(element)) if element.namespace == Namespace::HTML
299 && element.tag_name == "colgroup")
300 }
301 _ => false,
302 } =>
303 {
304 true
305 }
306 // A tbody element's start tag can be omitted if the first thing inside the tbody
307 // element is a tr element, and if the element is not immediately preceded by a
308 // tbody, thead, or tfoot element whose end tag has been omitted. (It can't be
309 // omitted if the element is empty.)
310 "tbody"
311 if match n.children.first() {
312 Some(Child::Element(element))
313 if element.namespace == Namespace::HTML && element.tag_name == "tr" =>
314 {
315 !matches!(prev, Some(Child::Element(element)) if element.namespace == Namespace::HTML
316 && matches!(
317 &*element.tag_name,
318 "tbody" | "thead" | "tfoot"
319 ))
320 }
321 _ => false,
322 } =>
323 {
324 true
325 }
326 _ => false,
327 };
328
329 let is_void_element = match n.namespace {
330 Namespace::HTML => matches!(
331 &*n.tag_name,
332 "area"
333 | "base"
334 | "basefont"
335 | "bgsound"
336 | "br"
337 | "col"
338 | "embed"
339 | "frame"
340 | "hr"
341 | "img"
342 | "input"
343 | "keygen"
344 | "link"
345 | "meta"
346 | "param"
347 | "source"
348 | "track"
349 | "wbr"
350 ),
351 Namespace::SVG => n.children.is_empty(),
352 Namespace::MATHML => n.children.is_empty(),
353 _ => false,
354 };
355
356 if !can_omit_start_tag {
357 write_raw!(self, "<");
358 write_raw!(self, &n.tag_name);
359
360 if has_attributes {
361 space!(self);
362
363 self.emit_list(&n.attributes, ListFormat::SpaceDelimited)?;
364 }
365
366 if (matches!(n.namespace, Namespace::SVG | Namespace::MATHML) && is_void_element)
367 || (self.self_closing_void_elements
368 && n.is_self_closing
369 && is_void_element
370 && matches!(n.namespace, Namespace::HTML))
371 {
372 if self.config.minify {
373 let need_space = match n.attributes.last() {
374 Some(Attribute {
375 value: Some(value), ..
376 }) => !value.chars().any(|c| match c {
377 c if c.is_ascii_whitespace() => true,
378 '`' | '=' | '<' | '>' | '"' | '\'' => true,
379 _ => false,
380 }),
381 _ => false,
382 };
383
384 if need_space {
385 write_raw!(self, " ");
386 }
387 } else {
388 write_raw!(self, " ");
389 }
390
391 write_raw!(self, "/");
392 }
393
394 write_raw!(self, ">");
395
396 if !self.config.minify && n.namespace == Namespace::HTML && n.tag_name == "html" {
397 newline!(self);
398 }
399 }
400
401 if is_void_element {
402 return Ok(());
403 }
404
405 if !self.is_plaintext {
406 self.is_plaintext = matches!(&*n.tag_name, "plaintext");
407 }
408
409 if let Some(content) = &n.content {
410 emit!(self, content);
411 } else if !n.children.is_empty() {
412 let ctx = self.create_context_for_element(n);
413
414 let need_extra_newline =
415 n.namespace == Namespace::HTML && matches!(&*n.tag_name, "textarea" | "pre");
416
417 if need_extra_newline {
418 if let Some(Child::Text(Text { data, .. })) = &n.children.first() {
419 if data.contains('\n') {
420 newline!(self);
421 } else {
422 formatting_newline!(self);
423 }
424 }
425 }
426
427 if self.tag_omission {
428 self.with_ctx(ctx)
429 .emit_list_for_tag_omission(TagOmissionParent::Element(n))?;
430 } else {
431 self.with_ctx(ctx)
432 .emit_list(&n.children, ListFormat::NotDelimited)?;
433 }
434 }
435
436 let can_omit_end_tag = self.is_plaintext
437 || (self.tag_omission
438 && n.namespace == Namespace::HTML
439 && match &*n.tag_name {
440 // Tag omission in text/html:
441
442 // An html element's end tag can be omitted if the html element is not
443 // immediately followed by a comment.
444 //
445 // A body element's end tag can be omitted if the body element is not
446 // immediately followed by a comment.
447 "html" => !matches!(next, Some(Child::Comment(..))),
448 "body" if !self.keep_head_and_body => !matches!(next, Some(Child::Comment(..))),
449 // A head element's end tag can be omitted if the head element is not
450 // immediately followed by ASCII whitespace or a comment.
451 "head" if !self.keep_head_and_body => match next {
452 Some(Child::Text(text))
453 if text.data.chars().next().unwrap().is_ascii_whitespace() =>
454 {
455 false
456 }
457 Some(Child::Comment(..)) => false,
458 _ => true,
459 },
460 // A p element's end tag can be omitted if the p element is immediately followed
461 // by an address, article, aside, blockquote, details, div, dl, fieldset,
462 // figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, hr,
463 // main, menu, nav, ol, p, pre, section, table, or ul element, or if there is no
464 // more content in the parent element and the parent element is an HTML element
465 // that is not an a, audio, del, ins, map, noscript, or video element, or an
466 // autonomous custom element.
467 "p" => match next {
468 Some(Child::Element(Element {
469 namespace,
470 tag_name,
471 ..
472 })) if *namespace == Namespace::HTML
473 && matches!(
474 &**tag_name,
475 "address"
476 | "article"
477 | "aside"
478 | "blockquote"
479 | "details"
480 | "div"
481 | "dl"
482 | "fieldset"
483 | "figcaption"
484 | "figure"
485 | "footer"
486 | "form"
487 | "h1"
488 | "h2"
489 | "h3"
490 | "h4"
491 | "h5"
492 | "h6"
493 | "header"
494 | "hgroup"
495 | "hr"
496 | "main"
497 | "menu"
498 | "nav"
499 | "ol"
500 | "p"
501 | "pre"
502 | "section"
503 | "table"
504 | "ul"
505 ) =>
506 {
507 true
508 }
509 None if match parent {
510 Some(Element {
511 namespace,
512 tag_name,
513 ..
514 }) if is_html_tag_name(*namespace, tag_name)
515 // Keep </p> under span to preserve parser stability for invalid
516 // markup such as `<span><p>...` (#11748).
517 && !matches!(
518 &**tag_name,
519 "a" | "audio"
520 | "acronym"
521 | "big"
522 | "del"
523 | "font"
524 | "ins"
525 | "tt"
526 | "strike"
527 | "map"
528 | "noscript"
529 | "span"
530 | "video"
531 | "kbd"
532 | "rbc"
533 ) =>
534 {
535 true
536 }
537 _ => false,
538 } =>
539 {
540 true
541 }
542 _ => false,
543 },
544 // An li element's end tag can be omitted if the li element is immediately
545 // followed by another li element or if there is no more content in the parent
546 // element.
547 "li" if match parent {
548 Some(Element {
549 namespace,
550 tag_name,
551 ..
552 }) if *namespace == Namespace::HTML
553 && matches!(&**tag_name, "ul" | "ol" | "menu") =>
554 {
555 true
556 }
557 _ => false,
558 } =>
559 {
560 match next {
561 Some(Child::Element(Element {
562 namespace,
563 tag_name,
564 ..
565 })) if *namespace == Namespace::HTML && *tag_name == "li" => true,
566 None => true,
567 _ => false,
568 }
569 }
570 // A dt element's end tag can be omitted if the dt element is immediately
571 // followed by another dt element or a dd element.
572 "dt" => match next {
573 Some(Child::Element(Element {
574 namespace,
575 tag_name,
576 ..
577 })) if *namespace == Namespace::HTML
578 && (*tag_name == "dt" || *tag_name == "dd") =>
579 {
580 true
581 }
582 _ => false,
583 },
584 // A dd element's end tag can be omitted if the dd element is immediately
585 // followed by another dd element or a dt element, or if there is no more
586 // content in the parent element.
587 "dd" => match next {
588 Some(Child::Element(Element {
589 namespace,
590 tag_name,
591 ..
592 })) if *namespace == Namespace::HTML
593 && (*tag_name == "dd" || *tag_name == "dt") =>
594 {
595 true
596 }
597 None => true,
598 _ => false,
599 },
600 // An rt element's end tag can be omitted if the rt element is immediately
601 // followed by an rt or rp element, or if there is no more content in the parent
602 // element.
603 //
604 // An rp element's end tag can be omitted if the rp element is immediately
605 // followed by an rt or rp element, or if there is no more content in the parent
606 // element.
607 "rt" | "rp" => match next {
608 Some(Child::Element(Element {
609 namespace,
610 tag_name,
611 ..
612 })) if *namespace == Namespace::HTML
613 && (*tag_name == "rt" || *tag_name == "rp") =>
614 {
615 true
616 }
617 None => true,
618 _ => false,
619 },
620 // The end tag can be omitted if the element is immediately followed by an <rt>,
621 // <rtc>, or <rp> element or another <rb> element, or if there is no more
622 // content in the parent element.
623 "rb" => match next {
624 Some(Child::Element(Element {
625 namespace,
626 tag_name,
627 ..
628 })) if *namespace == Namespace::HTML
629 && (*tag_name == "rt"
630 || *tag_name == "rtc"
631 || *tag_name == "rp"
632 || *tag_name == "rb") =>
633 {
634 true
635 }
636 None => true,
637 _ => false,
638 },
639 // The closing tag can be omitted if it is immediately followed by a <rb>,
640 // <rtc> or <rt> element opening tag or by its parent
641 // closing tag.
642 "rtc" => match next {
643 Some(Child::Element(Element {
644 namespace,
645 tag_name,
646 ..
647 })) if *namespace == Namespace::HTML
648 && (*tag_name == "rb" || *tag_name == "rtc" || *tag_name == "rt") =>
649 {
650 true
651 }
652 None => true,
653 _ => false,
654 },
655 // An optgroup element's end tag can be omitted if the optgroup element is
656 // immediately followed by another optgroup element, or if there is no more
657 // content in the parent element.
658 "optgroup" => match next {
659 Some(Child::Element(Element {
660 namespace,
661 tag_name,
662 ..
663 })) if *namespace == Namespace::HTML && *tag_name == "optgroup" => true,
664 None => true,
665 _ => false,
666 },
667 // An option element's end tag can be omitted if the option element is
668 // immediately followed by another option element, or if it is immediately
669 // followed by an optgroup element, or if there is no more content in the parent
670 // element.
671 "option" => match next {
672 Some(Child::Element(Element {
673 namespace,
674 tag_name,
675 ..
676 })) if *namespace == Namespace::HTML
677 && (*tag_name == "option" || *tag_name == "optgroup") =>
678 {
679 true
680 }
681 None => true,
682 _ => false,
683 },
684 // A caption element's end tag can be omitted if the caption element is not
685 // immediately followed by ASCII whitespace or a comment.
686 //
687 // A colgroup element's end tag can be omitted if the colgroup element is not
688 // immediately followed by ASCII whitespace or a comment.
689 "caption" | "colgroup" => match next {
690 Some(Child::Text(text))
691 if text.data.chars().next().unwrap().is_ascii_whitespace() =>
692 {
693 false
694 }
695 Some(Child::Comment(..)) => false,
696 _ => true,
697 },
698 // A tbody element's end tag can be omitted if the tbody element is immediately
699 // followed by a tbody or tfoot element, or if there is no more content in the
700 // parent element.
701 "tbody" => match next {
702 Some(Child::Element(Element {
703 namespace,
704 tag_name,
705 ..
706 })) if *namespace == Namespace::HTML
707 && (*tag_name == "tbody" || *tag_name == "tfoot") =>
708 {
709 true
710 }
711 None => true,
712 _ => false,
713 },
714 // A thead element's end tag can be omitted if the thead element is immediately
715 // followed by a tbody or tfoot element.
716 "thead" => match next {
717 Some(Child::Element(Element {
718 namespace,
719 tag_name,
720 ..
721 })) if *namespace == Namespace::HTML
722 && (*tag_name == "tbody" || *tag_name == "tfoot") =>
723 {
724 true
725 }
726 _ => false,
727 },
728 // A tfoot element's end tag can be omitted if there is no more content in the
729 // parent element.
730 "tfoot" => next.is_none(),
731 // A tr element's end tag can be omitted if the tr element is immediately
732 // followed by another tr element, or if there is no more content in the parent
733 // element.
734 "tr" => match next {
735 Some(Child::Element(Element {
736 namespace,
737 tag_name,
738 ..
739 })) if *namespace == Namespace::HTML && *tag_name == "tr" => true,
740 None => true,
741 _ => false,
742 },
743 // A th element's end tag can be omitted if the th element is immediately
744 // followed by a td or th element, or if there is no more content in the parent
745 // element.
746 "td" | "th" => match next {
747 Some(Child::Element(Element {
748 namespace,
749 tag_name,
750 ..
751 })) if *namespace == Namespace::HTML
752 && (*tag_name == "td" || *tag_name == "th") =>
753 {
754 true
755 }
756 None => true,
757 _ => false,
758 },
759 _ => false,
760 });
761
762 if can_omit_end_tag {
763 return Ok(());
764 }
765
766 write_raw!(self, "<");
767 write_raw!(self, "/");
768 write_raw!(self, &n.tag_name);
769 write_raw!(self, ">");
770
771 Ok(())
772 }
773
774 #[emitter]
775 fn emit_element(&mut self, n: &Element) -> Result {
776 self.basic_emit_element(n, None, None, None)?;
777 }
778
779 #[emitter]
780 fn emit_attribute(&mut self, n: &Attribute) -> Result {
781 let mut attribute = String::with_capacity(
782 if let Some(prefix) = &n.prefix {
783 prefix.len() + 1
784 } else {
785 0
786 } + n.name.len()
787 + if let Some(value) = &n.value {
788 value.len() + 1
789 } else {
790 0
791 },
792 );
793
794 if let Some(prefix) = &n.prefix {
795 attribute.push_str(prefix);
796 attribute.push(':');
797 }
798
799 attribute.push_str(&n.name);
800
801 if let Some(value) = &n.value {
802 attribute.push('=');
803
804 if self.config.minify {
805 let (minifier, quote) = minify_attribute_value(value, self.quotes);
806
807 if let Some(quote) = quote {
808 attribute.push(quote);
809 }
810
811 attribute.push_str(&minifier);
812
813 if let Some(quote) = quote {
814 attribute.push(quote);
815 }
816 } else {
817 let normalized = escape_string(value, true);
818
819 attribute.push('"');
820 attribute.push_str(&normalized);
821 attribute.push('"');
822 }
823 }
824
825 write_multiline_raw!(self, n.span, &attribute);
826 }
827
828 #[emitter]
829 fn emit_text(&mut self, n: &Text) -> Result {
830 if self.ctx.need_escape_text {
831 if self.config.minify {
832 write_multiline_raw!(self, n.span, &minify_text(&n.data));
833 } else {
834 write_multiline_raw!(self, n.span, &escape_string(&n.data, false));
835 }
836 } else {
837 write_multiline_raw!(self, n.span, &n.data);
838 }
839 }
840
841 #[emitter]
842 fn emit_comment(&mut self, n: &Comment) -> Result {
843 let mut comment = String::with_capacity(n.data.len() + 7);
844
845 comment.push_str("<!--");
846 comment.push_str(&n.data);
847 comment.push_str("-->");
848
849 write_multiline_raw!(self, n.span, &comment);
850 }
851
852 fn create_context_for_element(&self, n: &Element) -> Ctx {
853 let need_escape_text = match &*n.tag_name {
854 "style" | "script" | "xmp" | "iframe" | "noembed" | "noframes" | "plaintext" => false,
855 "noscript" => !self.config.scripting_enabled,
856 _ if self.is_plaintext => false,
857 _ => true,
858 };
859
860 Ctx {
861 need_escape_text,
862 ..self.ctx
863 }
864 }
865
866 fn emit_list_for_tag_omission(&mut self, parent: TagOmissionParent) -> Result {
867 let nodes = match &parent {
868 TagOmissionParent::Document(document) => &document.children,
869 TagOmissionParent::DocumentFragment(document_fragment) => &document_fragment.children,
870 TagOmissionParent::Element(element) => &element.children,
871 };
872 let parent = match parent {
873 TagOmissionParent::Element(element) => Some(element),
874 _ => None,
875 };
876
877 for (idx, node) in nodes.iter().enumerate() {
878 match node {
879 Child::Element(element) => {
880 let prev = if idx > 0 { nodes.get(idx - 1) } else { None };
881 let next = nodes.get(idx + 1);
882
883 self.basic_emit_element(element, parent, prev, next)?;
884 }
885 _ => {
886 emit!(self, node)
887 }
888 }
889 }
890
891 Ok(())
892 }
893
894 fn emit_list<N>(&mut self, nodes: &[N], format: ListFormat) -> Result
895 where
896 Self: Emit<N>,
897 N: Spanned,
898 {
899 for (idx, node) in nodes.iter().enumerate() {
900 if idx != 0 {
901 self.write_delim(format)?;
902
903 if format & ListFormat::LinesMask == ListFormat::MultiLine {
904 formatting_newline!(self);
905 }
906 }
907
908 emit!(self, node)
909 }
910
911 Ok(())
912 }
913
914 fn write_delim(&mut self, f: ListFormat) -> Result {
915 match f & ListFormat::DelimitersMask {
916 ListFormat::None => {}
917 ListFormat::SpaceDelimited => {
918 space!(self)
919 }
920 _ => unreachable!(),
921 }
922
923 Ok(())
924 }
925}
926
927#[allow(clippy::unused_peekable)]
928fn minify_attribute_value(value: &str, quotes: bool) -> (Cow<'_, str>, Option<char>) {
929 if value.is_empty() {
930 return (Cow::Borrowed(value), Some('"'));
931 }
932
933 // Fast-path
934 if !quotes
935 && value.chars().all(|c| match c {
936 '&' | '`' | '=' | '<' | '>' | '"' | '\'' => false,
937 c if c.is_ascii_whitespace() => false,
938 _ => true,
939 })
940 {
941 return (Cow::Borrowed(value), None);
942 }
943
944 let mut minified = String::with_capacity(value.len());
945
946 let mut unquoted = true;
947 let mut dq = 0;
948 let mut sq = 0;
949
950 let mut chars = value.chars().peekable();
951
952 while let Some(c) = chars.next() {
953 match c {
954 '&' => {
955 let next = chars.next();
956
957 if let Some(next) = next {
958 if matches!(next, '#' | 'a'..='z' | 'A'..='Z') {
959 minified.push_str(&minify_amp(next, &mut chars));
960 } else {
961 minified.push('&');
962 minified.push(next);
963 }
964 } else {
965 minified.push('&');
966 }
967
968 continue;
969 }
970 c if c.is_ascii_whitespace() => {
971 unquoted = false;
972 }
973 '`' | '=' | '<' | '>' => {
974 unquoted = false;
975 }
976 '"' => {
977 unquoted = false;
978 dq += 1;
979 }
980 '\'' => {
981 unquoted = false;
982 sq += 1;
983 }
984
985 _ => {}
986 };
987
988 minified.push(c);
989 }
990
991 if !quotes && unquoted {
992 return (Cow::Owned(minified), None);
993 }
994
995 if dq > sq {
996 (Cow::Owned(minified.replace('\'', "'")), Some('\''))
997 } else {
998 (Cow::Owned(minified.replace('"', """)), Some('"'))
999 }
1000}
1001
1002#[allow(clippy::unused_peekable)]
1003fn minify_text(value: &str) -> Cow<'_, str> {
1004 // Fast-path
1005 if value.is_empty() {
1006 return Cow::Borrowed(value);
1007 }
1008
1009 // Fast-path
1010 if value.chars().all(|c| match c {
1011 '&' | '<' => false,
1012 _ => true,
1013 }) {
1014 return Cow::Borrowed(value);
1015 }
1016
1017 let mut result = String::with_capacity(value.len());
1018 let mut chars = value.chars().peekable();
1019
1020 while let Some(c) = chars.next() {
1021 match c {
1022 '&' => {
1023 let next = chars.next();
1024
1025 if let Some(next) = next {
1026 if matches!(next, '#' | 'a'..='z' | 'A'..='Z') {
1027 result.push_str(&minify_amp(next, &mut chars));
1028 } else {
1029 result.push('&');
1030 result.push(next);
1031 }
1032 } else {
1033 result.push('&');
1034 }
1035 }
1036 '<' => {
1037 result.push_str("<");
1038 }
1039 _ => result.push(c),
1040 }
1041 }
1042
1043 Cow::Owned(result)
1044}
1045
1046fn minify_amp(next: char, chars: &mut Peekable<Chars>) -> String {
1047 let mut result = String::with_capacity(7);
1048
1049 match next {
1050 hash @ '#' => {
1051 match chars.next() {
1052 // HTML CODE
1053 // Prevent `&#38;` -> `&`
1054 Some(number @ '0'..='9') => {
1055 result.push_str("&");
1056 result.push(hash);
1057 result.push(number);
1058 }
1059 Some(x @ 'x' | x @ 'X') => {
1060 match chars.peek() {
1061 // HEX CODE
1062 // Prevent `&#x38;` -> `8`
1063 Some(c) if c.is_ascii_hexdigit() => {
1064 result.push_str("&");
1065 result.push(hash);
1066 result.push(x);
1067 }
1068 _ => {
1069 result.push('&');
1070 result.push(hash);
1071 result.push(x);
1072 }
1073 }
1074 }
1075 any => {
1076 result.push('&');
1077 result.push(hash);
1078
1079 if let Some(any) = any {
1080 result.push(any);
1081 }
1082 }
1083 }
1084 }
1085 // Named entity
1086 // Prevent `&current` -> `¤t`
1087 c @ 'a'..='z' | c @ 'A'..='Z' => {
1088 let mut entity_temporary_buffer = String::with_capacity(33);
1089
1090 entity_temporary_buffer.push('&');
1091 entity_temporary_buffer.push(c);
1092
1093 let mut found_entity = false;
1094
1095 // No need to validate input, because we reset position if nothing was found
1096 for c in chars {
1097 entity_temporary_buffer.push(c);
1098
1099 if HTML_ENTITIES.get(&entity_temporary_buffer).is_some() {
1100 found_entity = true;
1101
1102 break;
1103 } else {
1104 // We stop when:
1105 //
1106 // - not ascii alphanumeric
1107 // - we consume more characters than the longest entity
1108 if !c.is_ascii_alphanumeric() || entity_temporary_buffer.len() > 32 {
1109 break;
1110 }
1111 }
1112 }
1113
1114 if found_entity {
1115 result.push_str("&");
1116 result.push_str(&entity_temporary_buffer[1..]);
1117 } else {
1118 result.push('&');
1119 result.push_str(&entity_temporary_buffer[1..]);
1120 }
1121 }
1122 any => {
1123 result.push('&');
1124 result.push(any);
1125 }
1126 }
1127
1128 result
1129}
1130
1131// Escaping a string (for the purposes of the algorithm above) consists of
1132// running the following steps:
1133//
1134// 1. Replace any occurrence of the "&" character by the string "&".
1135//
1136// 2. Replace any occurrences of the U+00A0 NO-BREAK SPACE character by the
1137// string " ".
1138//
1139// 3. If the algorithm was invoked in the attribute mode, replace any
1140// occurrences of the """ character by the string """.
1141//
1142// 4. If the algorithm was not invoked in the attribute mode, replace any
1143// occurrences of the "<" character by the string "<", and any occurrences of
1144// the ">" character by the string ">".
1145fn escape_string(value: &str, is_attribute_mode: bool) -> Cow<'_, str> {
1146 // Fast-path
1147 if value.is_empty() {
1148 return Cow::Borrowed(value);
1149 }
1150
1151 if value.chars().all(|c| match c {
1152 '&' | '\u{00A0}' => false,
1153 '"' if is_attribute_mode => false,
1154 '<' if !is_attribute_mode => false,
1155 '>' if !is_attribute_mode => false,
1156 _ => true,
1157 }) {
1158 return Cow::Borrowed(value);
1159 }
1160
1161 let mut result = String::with_capacity(value.len());
1162
1163 for c in value.chars() {
1164 match c {
1165 '&' => {
1166 result.push_str("&");
1167 }
1168 '\u{00A0}' => result.push_str(" "),
1169 '"' if is_attribute_mode => result.push_str("""),
1170 '<' if !is_attribute_mode => {
1171 result.push_str("<");
1172 }
1173 '>' if !is_attribute_mode => {
1174 result.push_str(">");
1175 }
1176 _ => result.push(c),
1177 }
1178 }
1179
1180 Cow::Owned(result)
1181}
1182
1183fn is_html_tag_name(namespace: Namespace, tag_name: &Atom) -> bool {
1184 if namespace != Namespace::HTML {
1185 return false;
1186 }
1187
1188 matches!(
1189 &**tag_name,
1190 "a" | "abbr"
1191 | "acronym"
1192 | "address"
1193 | "applet"
1194 | "area"
1195 | "article"
1196 | "aside"
1197 | "audio"
1198 | "b"
1199 | "base"
1200 | "basefont"
1201 | "bdi"
1202 | "bdo"
1203 | "big"
1204 | "blockquote"
1205 | "body"
1206 | "br"
1207 | "button"
1208 | "canvas"
1209 | "caption"
1210 | "center"
1211 | "cite"
1212 | "code"
1213 | "col"
1214 | "colgroup"
1215 | "data"
1216 | "datalist"
1217 | "dd"
1218 | "del"
1219 | "details"
1220 | "dfn"
1221 | "dialog"
1222 | "dir"
1223 | "div"
1224 | "dl"
1225 | "dt"
1226 | "em"
1227 | "embed"
1228 | "fieldset"
1229 | "figcaption"
1230 | "figure"
1231 | "font"
1232 | "footer"
1233 | "form"
1234 | "frame"
1235 | "frameset"
1236 | "h1"
1237 | "h2"
1238 | "h3"
1239 | "h4"
1240 | "h5"
1241 | "h6"
1242 | "head"
1243 | "header"
1244 | "hgroup"
1245 | "hr"
1246 | "html"
1247 | "i"
1248 | "iframe"
1249 | "image"
1250 | "img"
1251 | "input"
1252 | "ins"
1253 | "isindex"
1254 | "kbd"
1255 | "keygen"
1256 | "label"
1257 | "legend"
1258 | "li"
1259 | "link"
1260 | "listing"
1261 | "main"
1262 | "map"
1263 | "mark"
1264 | "marquee"
1265 | "menu"
1266 // Removed from spec, but we keep here to track it
1267 // | "menuitem"
1268 | "meta"
1269 | "meter"
1270 | "nav"
1271 | "nobr"
1272 | "noembed"
1273 | "noframes"
1274 | "noscript"
1275 | "object"
1276 | "ol"
1277 | "optgroup"
1278 | "option"
1279 | "output"
1280 | "p"
1281 | "param"
1282 | "picture"
1283 | "plaintext"
1284 | "pre"
1285 | "progress"
1286 | "q"
1287 | "rb"
1288 | "rbc"
1289 | "rp"
1290 | "rt"
1291 | "rtc"
1292 | "ruby"
1293 | "s"
1294 | "samp"
1295 | "script"
1296 | "section"
1297 | "select"
1298 | "small"
1299 | "source"
1300 | "span"
1301 | "strike"
1302 | "strong"
1303 | "style"
1304 | "sub"
1305 | "summary"
1306 | "sup"
1307 | "table"
1308 | "tbody"
1309 | "td"
1310 | "template"
1311 | "textarea"
1312 | "tfoot"
1313 | "th"
1314 | "thead"
1315 | "time"
1316 | "title"
1317 | "tr"
1318 | "track"
1319 | "tt"
1320 | "u"
1321 | "ul"
1322 | "var"
1323 | "video"
1324 | "wbr"
1325 | "xmp"
1326 )
1327}