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