Skip to main content

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('\'', "&apos;")), Some('\''))
997    } else {
998        (Cow::Owned(minified.replace('"', "&quot;")), 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("&lt;");
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 `&amp;#38;` -> `&#38`
1054                Some(number @ '0'..='9') => {
1055                    result.push_str("&amp;");
1056                    result.push(hash);
1057                    result.push(number);
1058                }
1059                Some(x @ 'x' | x @ 'X') => {
1060                    match chars.peek() {
1061                        // HEX CODE
1062                        // Prevent `&amp;#x38;` -> `&#x38`
1063                        Some(c) if c.is_ascii_hexdigit() => {
1064                            result.push_str("&amp;");
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 `&amp;current` -> `&current`
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("&amp;");
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 "&amp;".
1135//
1136// 2. Replace any occurrences of the U+00A0 NO-BREAK SPACE character by the
1137// string "&nbsp;".
1138//
1139// 3. If the algorithm was invoked in the attribute mode, replace any
1140// occurrences of the """ character by the string "&quot;".
1141//
1142// 4. If the algorithm was not invoked in the attribute mode, replace any
1143// occurrences of the "<" character by the string "&lt;", and any occurrences of
1144// the ">" character by the string "&gt;".
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("&amp;");
1167            }
1168            '\u{00A0}' => result.push_str("&nbsp;"),
1169            '"' if is_attribute_mode => result.push_str("&quot;"),
1170            '<' if !is_attribute_mode => {
1171                result.push_str("&lt;");
1172            }
1173            '>' if !is_attribute_mode => {
1174                result.push_str("&gt;");
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}