swc_html_minifier/
lib.rs

1#![deny(clippy::all)]
2
3use std::{borrow::Cow, cmp::Ordering, mem::take};
4
5use once_cell::sync::Lazy;
6use rustc_hash::FxHashMap;
7use serde_json::Value;
8use swc_atoms::{atom, Atom};
9use swc_common::{
10    comments::SingleThreadedComments, sync::Lrc, EqIgnoreSpan, FileName, FilePathMapping, Mark,
11    SourceMap, DUMMY_SP,
12};
13use swc_config::regex::CachedRegex;
14use swc_html_ast::*;
15use swc_html_parser::parser::ParserConfig;
16use swc_html_utils::{HTML_ELEMENTS_AND_ATTRIBUTES, SVG_ELEMENTS_AND_ATTRIBUTES};
17use swc_html_visit::{VisitMut, VisitMutWith};
18
19#[cfg(feature = "default-css-minifier")]
20use crate::option::CssOptions;
21use crate::option::{
22    CollapseWhitespaces, JsOptions, JsParserOptions, JsonOptions, MinifierType, MinifyCssOption,
23    MinifyJsOption, MinifyJsonOption, MinifyOptions, RemoveRedundantAttributes,
24};
25
26pub mod option;
27
28static ALLOW_TO_TRIM_HTML_ATTRIBUTES: &[(&str, &str)] = &[
29    ("head", "profile"),
30    ("audio", "src"),
31    ("embed", "src"),
32    ("iframe", "src"),
33    ("img", "src"),
34    ("input", "src"),
35    ("input", "usemap"),
36    ("input", "longdesc"),
37    ("script", "src"),
38    ("source", "src"),
39    ("track", "src"),
40    ("video", "src"),
41    ("video", "poster"),
42    ("td", "colspan"),
43    ("td", "rowspan"),
44    ("th", "colspan"),
45    ("th", "rowspan"),
46    ("col", "span"),
47    ("colgroup", "span"),
48    ("textarea", "cols"),
49    ("textarea", "rows"),
50    ("textarea", "maxlength"),
51    ("input", "size"),
52    ("input", "formaction"),
53    ("input", "maxlength"),
54    ("button", "formaction"),
55    ("select", "size"),
56    ("form", "action"),
57    ("object", "data"),
58    ("object", "codebase"),
59    ("object", "classid"),
60    ("applet", "codebase"),
61    ("a", "href"),
62    ("area", "href"),
63    ("link", "href"),
64    ("base", "href"),
65    ("q", "cite"),
66    ("blockquote", "cite"),
67    ("del", "cite"),
68    ("ins", "cite"),
69    ("img", "usemap"),
70    ("object", "usemap"),
71];
72
73static ALLOW_TO_TRIM_SVG_ATTRIBUTES: &[(&str, &str)] = &[("a", "href")];
74
75static COMMA_SEPARATED_HTML_ATTRIBUTES: &[(&str, &str)] = &[
76    ("img", "srcset"),
77    ("source", "srcset"),
78    ("img", "sizes"),
79    ("source", "sizes"),
80    ("link", "media"),
81    ("source", "media"),
82    ("style", "media"),
83];
84
85static COMMA_SEPARATED_SVG_ATTRIBUTES: &[(&str, &str)] = &[
86    ("style", "media"),
87    ("polyline", "points"),
88    ("polygon", "points"),
89];
90
91static SPACE_SEPARATED_HTML_ATTRIBUTES: &[(&str, &str)] = &[
92    ("a", "rel"),
93    ("a", "ping"),
94    ("area", "rel"),
95    ("area", "ping"),
96    ("link", "rel"),
97    ("link", "sizes"),
98    ("link", "blocking"),
99    ("iframe", "sandbox"),
100    ("td", "headers"),
101    ("th", "headers"),
102    ("output", "for"),
103    ("script", "blocking"),
104    ("style", "blocking"),
105    ("input", "autocomplete"),
106    ("form", "rel"),
107    ("form", "autocomplete"),
108];
109
110static SPACE_SEPARATED_SVG_ATTRIBUTES: &[(&str, &str)] = &[
111    ("svg", "preserveAspectRatio"),
112    ("svg", "viewBox"),
113    ("symbol", "preserveAspectRatio"),
114    ("symbol", "viewBox"),
115    ("image", "preserveAspectRatio"),
116    ("feImage", "preserveAspectRatio"),
117    ("marker", "preserveAspectRatio"),
118    ("pattern", "preserveAspectRatio"),
119    ("pattern", "viewBox"),
120    ("pattern", "patternTransform"),
121    ("view", "preserveAspectRatio"),
122    ("view", "viewBox"),
123    ("path", "d"),
124    // TODO improve me more
125    ("textPath", "path"),
126    ("animateMotion", "path"),
127    ("glyph", "d"),
128    ("missing-glyph", "d"),
129    ("feColorMatrix", "values"),
130    ("feConvolveMatrix", "kernelMatrix"),
131    ("text", "rotate"),
132    ("tspan", "rotate"),
133    ("feFuncA", "tableValues"),
134    ("feFuncB", "tableValues"),
135    ("feFuncG", "tableValues"),
136    ("feFuncR", "tableValues"),
137    ("linearGradient", "gradientTransform"),
138    ("radialGradient", "gradientTransform"),
139    ("font-face", "panose-1"),
140    ("a", "rel"),
141];
142
143static SEMICOLON_SEPARATED_SVG_ATTRIBUTES: &[(&str, &str)] = &[
144    ("animate", "keyTimes"),
145    ("animate", "keySplines"),
146    ("animate", "values"),
147    ("animate", "begin"),
148    ("animate", "end"),
149    ("animateColor", "keyTimes"),
150    ("animateColor", "keySplines"),
151    ("animateColor", "values"),
152    ("animateColor", "begin"),
153    ("animateColor", "end"),
154    ("animateMotion", "keyTimes"),
155    ("animateMotion", "keySplines"),
156    ("animateMotion", "values"),
157    ("animateMotion", "values"),
158    ("animateMotion", "end"),
159    ("animateTransform", "keyTimes"),
160    ("animateTransform", "keySplines"),
161    ("animateTransform", "values"),
162    ("animateTransform", "begin"),
163    ("animateTransform", "end"),
164    ("discard", "begin"),
165    ("set", "begin"),
166    ("set", "end"),
167];
168
169pub enum CssMinificationMode {
170    Stylesheet,
171    ListOfDeclarations,
172    MediaQueryList,
173}
174
175enum HtmlMinificationMode {
176    ConditionalComments,
177    DocumentIframeSrcdoc,
178}
179
180enum HtmlRoot {
181    Document(Document),
182    DocumentFragment(DocumentFragment),
183}
184
185#[inline(always)]
186fn is_whitespace(c: char) -> bool {
187    matches!(c, '\x09' | '\x0a' | '\x0c' | '\x0d' | '\x20')
188}
189
190#[derive(Debug, Copy, Clone)]
191struct WhitespaceMinificationMode {
192    pub trim: bool,
193    pub collapse: bool,
194}
195
196#[derive(Debug, Eq, PartialEq)]
197enum Display {
198    None,
199    Inline,
200    InlineBlock,
201    Block,
202    ListItem,
203    Ruby,
204    RubyBase,
205    RubyText,
206    Table,
207    TableColumnGroup,
208    TableCaption,
209    TableColumn,
210    TableRow,
211    TableCell,
212    TableHeaderGroup,
213    TableRowGroup,
214    TableFooterGroup,
215    Contents,
216}
217
218#[derive(Debug, Eq, PartialEq)]
219enum WhiteSpace {
220    Pre,
221    Normal,
222}
223
224pub static CONDITIONAL_COMMENT_START: Lazy<CachedRegex> =
225    Lazy::new(|| CachedRegex::new("^\\[if\\s[^\\]+]").unwrap());
226
227pub static CONDITIONAL_COMMENT_END: Lazy<CachedRegex> =
228    Lazy::new(|| CachedRegex::new("\\[endif]").unwrap());
229
230struct Minifier<'a, C: MinifyCss> {
231    options: &'a MinifyOptions<C::Options>,
232
233    current_element: Option<Element>,
234    latest_element: Option<Child>,
235    descendant_of_pre: bool,
236    attribute_name_counter: Option<FxHashMap<Atom, usize>>,
237
238    css_minifier: &'a C,
239}
240
241fn get_white_space(namespace: Namespace, tag_name: &str) -> WhiteSpace {
242    match namespace {
243        Namespace::HTML => match tag_name {
244            "textarea" | "code" | "pre" | "listing" | "plaintext" | "xmp" => WhiteSpace::Pre,
245            _ => WhiteSpace::Normal,
246        },
247        _ => WhiteSpace::Normal,
248    }
249}
250
251impl<C: MinifyCss> Minifier<'_, C> {
252    fn is_event_handler_attribute(&self, attribute: &Attribute) -> bool {
253        matches!(
254            &*attribute.name,
255            "onabort"
256                | "onautocomplete"
257                | "onautocompleteerror"
258                | "onauxclick"
259                | "onbeforematch"
260                | "oncancel"
261                | "oncanplay"
262                | "oncanplaythrough"
263                | "onchange"
264                | "onclick"
265                | "onclose"
266                | "oncontextlost"
267                | "oncontextmenu"
268                | "oncontextrestored"
269                | "oncuechange"
270                | "ondblclick"
271                | "ondrag"
272                | "ondragend"
273                | "ondragenter"
274                | "ondragexit"
275                | "ondragleave"
276                | "ondragover"
277                | "ondragstart"
278                | "ondrop"
279                | "ondurationchange"
280                | "onemptied"
281                | "onended"
282                | "onformdata"
283                | "oninput"
284                | "oninvalid"
285                | "onkeydown"
286                | "onkeypress"
287                | "onkeyup"
288                | "onmousewheel"
289                | "onmousedown"
290                | "onmouseenter"
291                | "onmouseleave"
292                | "onmousemove"
293                | "onmouseout"
294                | "onmouseover"
295                | "onmouseup"
296                | "onpause"
297                | "onplay"
298                | "onplaying"
299                | "onprogress"
300                | "onratechange"
301                | "onreset"
302                | "onsecuritypolicyviolation"
303                | "onseeked"
304                | "onseeking"
305                | "onselect"
306                | "onslotchange"
307                | "onstalled"
308                | "onsubmit"
309                | "onsuspend"
310                | "ontimeupdate"
311                | "ontoggle"
312                | "onvolumechange"
313                | "onwaiting"
314                | "onwebkitanimationend"
315                | "onwebkitanimationiteration"
316                | "onwebkitanimationstart"
317                | "onwebkittransitionend"
318                | "onwheel"
319                | "onblur"
320                | "onerror"
321                | "onfocus"
322                | "onload"
323                | "onloadeddata"
324                | "onloadedmetadata"
325                | "onloadstart"
326                | "onresize"
327                | "onscroll"
328                | "onafterprint"
329                | "onbeforeprint"
330                | "onbeforeunload"
331                | "onhashchange"
332                | "onlanguagechange"
333                | "onmessage"
334                | "onmessageerror"
335                | "onoffline"
336                | "ononline"
337                | "onpagehide"
338                | "onpageshow"
339                | "onpopstate"
340                | "onrejectionhandled"
341                | "onstorage"
342                | "onunhandledrejection"
343                | "onunload"
344                | "oncut"
345                | "oncopy"
346                | "onpaste"
347                | "onreadystatechange"
348                | "onvisibilitychange"
349                | "onshow"
350                | "onsort"
351                | "onbegin"
352                | "onend"
353                | "onrepeat"
354        )
355    }
356
357    fn is_boolean_attribute(&self, element: &Element, attribute: &Attribute) -> bool {
358        if element.namespace != Namespace::HTML {
359            return false;
360        }
361
362        if let Some(global_pseudo_element) = HTML_ELEMENTS_AND_ATTRIBUTES.get(&atom!("*")) {
363            if let Some(element) = global_pseudo_element.other.get(&attribute.name) {
364                if element.boolean.is_some() && element.boolean.unwrap() {
365                    return true;
366                }
367            }
368        }
369
370        if let Some(element) = HTML_ELEMENTS_AND_ATTRIBUTES.get(&element.tag_name) {
371            if let Some(element) = element.other.get(&attribute.name) {
372                if element.boolean.is_some() && element.boolean.unwrap() {
373                    return true;
374                }
375            }
376        }
377
378        false
379    }
380
381    fn is_trimable_separated_attribute(&self, element: &Element, attribute: &Attribute) -> bool {
382        match &*attribute.name {
383            "style" | "tabindex" | "itemid" => return true,
384            _ => {}
385        }
386
387        match element.namespace {
388            Namespace::HTML => {
389                ALLOW_TO_TRIM_HTML_ATTRIBUTES.contains(&(&element.tag_name, &attribute.name))
390            }
391            Namespace::SVG => {
392                ALLOW_TO_TRIM_SVG_ATTRIBUTES.contains(&(&element.tag_name, &attribute.name))
393            }
394            _ => false,
395        }
396    }
397
398    fn is_comma_separated_attribute(&self, element: &Element, attribute: &Attribute) -> bool {
399        match element.namespace {
400            Namespace::HTML => match &*attribute.name {
401                "content"
402                    if element.tag_name == "meta"
403                        && (self.element_has_attribute_with_value(
404                            element,
405                            "name",
406                            &["viewport", "keywords"],
407                        )) =>
408                {
409                    true
410                }
411                "imagesrcset"
412                    if element.tag_name == "link"
413                        && self.element_has_attribute_with_value(element, "rel", &["preload"]) =>
414                {
415                    true
416                }
417                "imagesizes"
418                    if element.tag_name == "link"
419                        && self.element_has_attribute_with_value(element, "rel", &["preload"]) =>
420                {
421                    true
422                }
423                "accept"
424                    if element.tag_name == "input"
425                        && self.element_has_attribute_with_value(element, "type", &["file"]) =>
426                {
427                    true
428                }
429                _ if attribute.name == "exportparts" => true,
430                _ => {
431                    COMMA_SEPARATED_HTML_ATTRIBUTES.contains(&(&element.tag_name, &attribute.name))
432                }
433            },
434            Namespace::SVG => {
435                COMMA_SEPARATED_SVG_ATTRIBUTES.contains(&(&element.tag_name, &attribute.name))
436            }
437            _ => false,
438        }
439    }
440
441    fn is_space_separated_attribute(&self, element: &Element, attribute: &Attribute) -> bool {
442        match &*attribute.name {
443            "class" | "itemprop" | "itemref" | "itemtype" | "part" | "accesskey"
444            | "aria-describedby" | "aria-labelledby" | "aria-owns" => return true,
445            _ => {}
446        }
447
448        match element.namespace {
449            Namespace::HTML => {
450                SPACE_SEPARATED_HTML_ATTRIBUTES.contains(&(&element.tag_name, &attribute.name))
451            }
452            Namespace::SVG => {
453                match &*attribute.name {
454                    "transform" | "stroke-dasharray" | "clip-path" | "requiredFeatures" => {
455                        return true
456                    }
457                    _ => {}
458                }
459
460                SPACE_SEPARATED_SVG_ATTRIBUTES.contains(&(&element.tag_name, &attribute.name))
461            }
462            _ => false,
463        }
464    }
465
466    fn is_semicolon_separated_attribute(&self, element: &Element, attribute: &Attribute) -> bool {
467        match element.namespace {
468            Namespace::SVG => {
469                SEMICOLON_SEPARATED_SVG_ATTRIBUTES.contains(&(&element.tag_name, &attribute.name))
470            }
471            _ => false,
472        }
473    }
474
475    fn is_attribute_value_unordered_set(&self, element: &Element, attribute: &Attribute) -> bool {
476        if matches!(
477            &*attribute.name,
478            "class" | "part" | "itemprop" | "itemref" | "itemtype"
479        ) {
480            return true;
481        }
482
483        match element.namespace {
484            Namespace::HTML => match &*element.tag_name {
485                "link" if attribute.name == "blocking" => true,
486                "script" if attribute.name == "blocking" => true,
487                "style" if attribute.name == "blocking" => true,
488                "output" if attribute.name == "for" => true,
489                "td" if attribute.name == "headers" => true,
490                "th" if attribute.name == "headers" => true,
491                "form" if attribute.name == "rel" => true,
492                "a" if attribute.name == "rel" => true,
493                "area" if attribute.name == "rel" => true,
494                "link" if attribute.name == "rel" => true,
495                "iframe" if attribute.name == "sandbox" => true,
496                "link"
497                    if self.element_has_attribute_with_value(
498                        element,
499                        "rel",
500                        &["icon", "apple-touch-icon", "apple-touch-icon-precomposed"],
501                    ) && attribute.name == "sizes" =>
502                {
503                    true
504                }
505                _ => false,
506            },
507            Namespace::SVG => {
508                matches!(&*element.tag_name, "a" if attribute.name == "rel")
509            }
510            _ => false,
511        }
512    }
513
514    fn is_crossorigin_attribute(&self, current_element: &Element, attribute: &Attribute) -> bool {
515        matches!(
516            (
517                current_element.namespace,
518                &*current_element.tag_name,
519                &*attribute.name,
520            ),
521            (
522                Namespace::HTML,
523                "img" | "audio" | "video" | "script" | "link",
524                "crossorigin",
525            ) | (Namespace::SVG, "image", "crossorigin")
526        )
527    }
528
529    fn element_has_attribute_with_value(
530        &self,
531        element: &Element,
532        attribute_name: &str,
533        attribute_value: &[&str],
534    ) -> bool {
535        element.attributes.iter().any(|attribute| {
536            &*attribute.name == attribute_name
537                && attribute.value.is_some()
538                && attribute_value
539                    .contains(&&*attribute.value.as_ref().unwrap().to_ascii_lowercase())
540        })
541    }
542
543    fn is_type_text_javascript(&self, value: &str) -> bool {
544        let value = value.trim().to_ascii_lowercase();
545        let value = if let Some(next) = value.split(';').next() {
546            next
547        } else {
548            &value
549        };
550
551        match value {
552            // Legacy JavaScript MIME types
553            "application/javascript"
554            | "application/ecmascript"
555            | "application/x-ecmascript"
556            | "application/x-javascript"
557            | "text/ecmascript"
558            | "text/javascript1.0"
559            | "text/javascript1.1"
560            | "text/javascript1.2"
561            | "text/javascript1.3"
562            | "text/javascript1.4"
563            | "text/javascript1.5"
564            | "text/jscript"
565            | "text/livescript"
566            | "text/x-ecmascript"
567            | "text/x-javascript" => true,
568            "text/javascript" => true,
569            _ => false,
570        }
571    }
572
573    fn is_type_text_css(&self, value: &Atom) -> bool {
574        let value = value.trim().to_ascii_lowercase();
575
576        matches!(&*value, "text/css")
577    }
578
579    fn is_default_attribute_value(&self, element: &Element, attribute: &Attribute) -> bool {
580        let attribute_value = match &attribute.value {
581            Some(value) => value,
582            _ => return false,
583        };
584
585        match element.namespace {
586            Namespace::HTML | Namespace::SVG => {
587                match &*element.tag_name {
588                    "html" => match &*attribute.name {
589                        "xmlns" => {
590                            if &*attribute_value.trim().to_ascii_lowercase()
591                                == "http://www.w3.org/1999/xhtml"
592                            {
593                                return true;
594                            }
595                        }
596                        "xmlns:xlink" => {
597                            if &*attribute_value.trim().to_ascii_lowercase()
598                                == "http://www.w3.org/1999/xlink"
599                            {
600                                return true;
601                            }
602                        }
603                        _ => {}
604                    },
605                    "script" => match &*attribute.name {
606                        "type" => {
607                            if self.is_type_text_javascript(attribute_value) {
608                                return true;
609                            }
610                        }
611                        "language" => match &*attribute_value.trim().to_ascii_lowercase() {
612                            "javascript" | "javascript1.2" | "javascript1.3" | "javascript1.4"
613                            | "javascript1.5" | "javascript1.6" | "javascript1.7" => return true,
614                            _ => {}
615                        },
616                        _ => {}
617                    },
618                    "link" => {
619                        if attribute.name == "type" && self.is_type_text_css(attribute_value) {
620                            return true;
621                        }
622                    }
623
624                    "svg" => {
625                        if attribute.name == "xmlns"
626                            && &*attribute_value.trim().to_ascii_lowercase()
627                                == "http://www.w3.org/2000/svg"
628                        {
629                            return true;
630                        }
631                    }
632                    _ => {}
633                }
634
635                let default_attributes = if element.namespace == Namespace::HTML {
636                    &HTML_ELEMENTS_AND_ATTRIBUTES
637                } else {
638                    &SVG_ELEMENTS_AND_ATTRIBUTES
639                };
640
641                let attributes = match default_attributes.get(&element.tag_name) {
642                    Some(element) => element,
643                    None => return false,
644                };
645
646                let attribute_info = if let Some(prefix) = &attribute.prefix {
647                    let mut with_namespace =
648                        String::with_capacity(prefix.len() + 1 + attribute.name.len());
649
650                    with_namespace.push_str(prefix);
651                    with_namespace.push(':');
652                    with_namespace.push_str(&attribute.name);
653
654                    attributes.other.get(&Atom::from(with_namespace))
655                } else {
656                    attributes.other.get(&attribute.name)
657                };
658
659                let attribute_info = match attribute_info {
660                    Some(attribute_info) => attribute_info,
661                    None => return false,
662                };
663
664                match (attribute_info.inherited, &attribute_info.initial) {
665                    (None, Some(initial)) | (Some(false), Some(initial)) => {
666                        let normalized_value = attribute_value.trim();
667
668                        match self.options.remove_redundant_attributes {
669                            RemoveRedundantAttributes::None => false,
670                            RemoveRedundantAttributes::Smart => {
671                                if initial == normalized_value {
672                                    // It is safe to remove deprecated redundant attributes, they
673                                    // should not be used
674                                    if attribute_info.deprecated == Some(true) {
675                                        return true;
676                                    }
677
678                                    // It it safe to remove svg redundant attributes, they used for
679                                    // styling
680                                    if element.namespace == Namespace::SVG {
681                                        return true;
682                                    }
683
684                                    // It it safe to remove redundant attributes for metadata
685                                    // elements
686                                    if element.namespace == Namespace::HTML
687                                        && matches!(
688                                            &*element.tag_name,
689                                            "base"
690                                                | "link"
691                                                | "noscript"
692                                                | "script"
693                                                | "style"
694                                                | "title"
695                                        )
696                                    {
697                                        return true;
698                                    }
699                                }
700
701                                false
702                            }
703                            RemoveRedundantAttributes::All => initial == normalized_value,
704                        }
705                    }
706                    _ => false,
707                }
708            }
709            _ => {
710                matches!(
711                    (
712                        element.namespace,
713                        &*element.tag_name,
714                        &*attribute.name,
715                        attribute_value.to_ascii_lowercase().trim()
716                    ),
717                    |(Namespace::MATHML, "math", "xmlns", "http://www.w3.org/1998/math/mathml")| (
718                        Namespace::MATHML,
719                        "math",
720                        "xlink",
721                        "http://www.w3.org/1999/xlink"
722                    )
723                )
724            }
725        }
726    }
727
728    fn is_javascript_url_element(&self, element: &Element) -> bool {
729        match (element.namespace, &*element.tag_name) {
730            (Namespace::HTML | Namespace::SVG, "a") => return true,
731            (Namespace::HTML, "iframe") => return true,
732            _ => {}
733        }
734
735        false
736    }
737
738    fn is_preserved_comment(&self, data: &Atom) -> bool {
739        if let Some(preserve_comments) = &self.options.preserve_comments {
740            return preserve_comments.iter().any(|regex| regex.is_match(data));
741        }
742
743        false
744    }
745
746    fn is_conditional_comment(&self, data: &Atom) -> bool {
747        if CONDITIONAL_COMMENT_START.is_match(data) || CONDITIONAL_COMMENT_END.is_match(data) {
748            return true;
749        }
750
751        false
752    }
753
754    fn need_collapse_whitespace(&self) -> bool {
755        !matches!(self.options.collapse_whitespaces, CollapseWhitespaces::None)
756    }
757
758    fn is_custom_element(&self, element: &Element) -> bool {
759        // https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
760        match &*element.tag_name {
761            "annotation-xml" | "color-profile" | "font-face" | "font-face-src"
762            | "font-face-uri" | "font-face-format" | "font-face-name" | "missing-glyph" => false,
763            _ => {
764                matches!(element.tag_name.chars().next(), Some('a'..='z'))
765                    && element.tag_name.contains('-')
766            }
767        }
768    }
769
770    fn get_display(&self, element: &Element) -> Display {
771        match element.namespace {
772            Namespace::HTML => {
773                match &*element.tag_name {
774                    "area" | "base" | "basefont" | "datalist" | "head" | "link" | "meta"
775                    | "noembed" | "noframes" | "param" | "rp" | "script" | "style" | "template"
776                    | "title" => Display::None,
777
778                    "a" | "abbr" | "acronym" | "b" | "bdi" | "bdo" | "cite" | "data" | "big"
779                    | "del" | "dfn" | "em" | "i" | "ins" | "kbd" | "mark" | "q" | "nobr"
780                    | "rtc" | "s" | "samp" | "small" | "span" | "strike" | "strong" | "sub"
781                    | "sup" | "time" | "tt" | "u" | "var" | "wbr" | "object" | "audio" | "code"
782                    | "label" | "br" | "img" | "video" | "noscript" | "picture" | "source"
783                    | "track" | "map" | "applet" | "bgsound" | "blink" | "canvas" | "command"
784                    | "content" | "embed" | "frame" | "iframe" | "image" | "isindex" | "keygen"
785                    | "output" | "rbc" | "shadow" | "spacer" => Display::Inline,
786
787                    "html" | "body" | "address" | "blockquote" | "center" | "div" | "figure"
788                    | "figcaption" | "footer" | "form" | "header" | "hr" | "legend" | "listing"
789                    | "main" | "p" | "plaintext" | "pre" | "xmp" | "details" | "summary"
790                    | "optgroup" | "option" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6"
791                    | "fieldset" | "ul" | "ol" | "menu" | "dir" | "dl" | "dt" | "dd"
792                    | "section" | "nav" | "hgroup" | "aside" | "article" | "dialog" | "element"
793                    | "font" | "frameset" => Display::Block,
794
795                    "li" => Display::ListItem,
796
797                    "button" | "meter" | "progress" | "select" | "textarea" | "input"
798                    | "marquee" => Display::InlineBlock,
799
800                    "ruby" => Display::Ruby,
801
802                    "rb" => Display::RubyBase,
803
804                    "rt" => Display::RubyText,
805
806                    "table" => Display::Table,
807
808                    "caption" => Display::TableCaption,
809
810                    "colgroup" => Display::TableColumnGroup,
811
812                    "col" => Display::TableColumn,
813
814                    "thead" => Display::TableHeaderGroup,
815
816                    "tbody" => Display::TableRowGroup,
817
818                    "tfoot" => Display::TableFooterGroup,
819
820                    "tr" => Display::TableRow,
821
822                    "td" | "th" => Display::TableCell,
823
824                    "slot" => Display::Contents,
825
826                    _ => Display::Inline,
827                }
828            }
829            Namespace::SVG => match &*element.tag_name {
830                "text" | "foreignObject" => Display::Block,
831                _ => Display::Inline,
832            },
833            _ => Display::Inline,
834        }
835    }
836
837    fn is_element_displayed(&self, element: &Element) -> bool {
838        match element.namespace {
839            Namespace::HTML => {
840                // https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#metadata_content
841                //
842                // Excluded:
843                // `noscript` - can be displayed if JavaScript disabled
844                // `script` - can insert markup using `document.write`
845                !matches!(
846                    &*element.tag_name,
847                    "base" | "command" | "link" | "meta" | "style" | "title" | "template"
848                )
849            }
850            Namespace::SVG => !matches!(&*element.tag_name, "style"),
851            _ => true,
852        }
853    }
854
855    #[allow(clippy::only_used_in_recursion)]
856    fn remove_leading_and_trailing_whitespaces(
857        &self,
858        children: &mut Vec<Child>,
859        only_first: bool,
860        only_last: bool,
861    ) {
862        if only_first {
863            if let Some(last) = children.first_mut() {
864                match last {
865                    Child::Text(text) => {
866                        text.data = text.data.trim_start_matches(is_whitespace).into();
867
868                        if text.data.is_empty() {
869                            children.remove(0);
870                        }
871                    }
872                    Child::Element(Element {
873                        namespace,
874                        tag_name,
875                        children,
876                        ..
877                    }) if get_white_space(*namespace, tag_name) == WhiteSpace::Normal => {
878                        self.remove_leading_and_trailing_whitespaces(children, true, false);
879                    }
880                    _ => {}
881                }
882            }
883        }
884
885        if only_last {
886            if let Some(last) = children.last_mut() {
887                match last {
888                    Child::Text(text) => {
889                        text.data = text.data.trim_end_matches(is_whitespace).into();
890
891                        if text.data.is_empty() {
892                            children.pop();
893                        }
894                    }
895                    Child::Element(Element {
896                        namespace,
897                        tag_name,
898                        children,
899                        ..
900                    }) if get_white_space(*namespace, tag_name) == WhiteSpace::Normal => {
901                        self.remove_leading_and_trailing_whitespaces(children, false, true);
902                    }
903                    _ => {}
904                }
905            }
906        }
907    }
908
909    fn get_prev_displayed_node<'a>(
910        &self,
911        children: &'a Vec<Child>,
912        index: usize,
913    ) -> Option<&'a Child> {
914        let prev = children.get(index);
915
916        match prev {
917            Some(Child::Comment(_)) => {
918                if index >= 1 {
919                    self.get_prev_displayed_node(children, index - 1)
920                } else {
921                    None
922                }
923            }
924            Some(Child::Element(element)) => {
925                if !self.is_element_displayed(element) && index >= 1 {
926                    self.get_prev_displayed_node(children, index - 1)
927                } else if !element.children.is_empty() {
928                    self.get_prev_displayed_node(&element.children, element.children.len() - 1)
929                } else {
930                    prev
931                }
932            }
933            Some(_) => prev,
934            _ => None,
935        }
936    }
937
938    fn get_last_displayed_text_node<'a>(
939        &self,
940        children: &'a Vec<Child>,
941        index: usize,
942    ) -> Option<&'a Text> {
943        let prev = children.get(index);
944
945        match prev {
946            Some(Child::Comment(_)) => {
947                if index >= 1 {
948                    self.get_last_displayed_text_node(children, index - 1)
949                } else {
950                    None
951                }
952            }
953            Some(Child::Element(element)) => {
954                if !self.is_element_displayed(element) && index >= 1 {
955                    self.get_last_displayed_text_node(children, index - 1)
956                } else if !element.children.is_empty() {
957                    for index in (0..=element.children.len() - 1).rev() {
958                        if let Some(text) =
959                            self.get_last_displayed_text_node(&element.children, index)
960                        {
961                            return Some(text);
962                        }
963                    }
964
965                    None
966                } else {
967                    None
968                }
969            }
970            Some(Child::Text(text)) => Some(text),
971            _ => None,
972        }
973    }
974
975    fn get_first_displayed_text_node<'a>(
976        &self,
977        children: &'a Vec<Child>,
978        index: usize,
979    ) -> Option<&'a Text> {
980        let next = children.get(index);
981
982        match next {
983            Some(Child::Comment(_)) => self.get_first_displayed_text_node(children, index + 1),
984            Some(Child::Element(element)) => {
985                if !self.is_element_displayed(element) && index >= 1 {
986                    self.get_first_displayed_text_node(children, index - 1)
987                } else if !element.children.is_empty() {
988                    for index in 0..=element.children.len() - 1 {
989                        if let Some(text) =
990                            self.get_first_displayed_text_node(&element.children, index)
991                        {
992                            return Some(text);
993                        }
994                    }
995
996                    None
997                } else {
998                    None
999                }
1000            }
1001            Some(Child::Text(text)) => Some(text),
1002            _ => None,
1003        }
1004    }
1005
1006    fn get_next_displayed_node<'a>(
1007        &self,
1008        children: &'a Vec<Child>,
1009        index: usize,
1010    ) -> Option<&'a Child> {
1011        let next = children.get(index);
1012
1013        match next {
1014            Some(Child::Comment(_)) => self.get_next_displayed_node(children, index + 1),
1015            Some(Child::Element(element)) if !self.is_element_displayed(element) => {
1016                self.get_next_displayed_node(children, index + 1)
1017            }
1018            Some(_) => next,
1019            _ => None,
1020        }
1021    }
1022
1023    fn get_whitespace_minification_for_tag(&self, element: &Element) -> WhitespaceMinificationMode {
1024        let default_collapse = match self.options.collapse_whitespaces {
1025            CollapseWhitespaces::All
1026            | CollapseWhitespaces::Smart
1027            | CollapseWhitespaces::Conservative
1028            | CollapseWhitespaces::AdvancedConservative => true,
1029            CollapseWhitespaces::OnlyMetadata | CollapseWhitespaces::None => false,
1030        };
1031        let default_trim = match self.options.collapse_whitespaces {
1032            CollapseWhitespaces::All => true,
1033            CollapseWhitespaces::Smart
1034            | CollapseWhitespaces::Conservative
1035            | CollapseWhitespaces::OnlyMetadata
1036            | CollapseWhitespaces::AdvancedConservative
1037            | CollapseWhitespaces::None => false,
1038        };
1039
1040        match element.namespace {
1041            Namespace::HTML => match &*element.tag_name {
1042                "script" | "style" => WhitespaceMinificationMode {
1043                    collapse: false,
1044                    trim: !matches!(
1045                        self.options.collapse_whitespaces,
1046                        CollapseWhitespaces::None | CollapseWhitespaces::OnlyMetadata
1047                    ),
1048                },
1049                _ => {
1050                    if get_white_space(element.namespace, &element.tag_name) == WhiteSpace::Pre {
1051                        WhitespaceMinificationMode {
1052                            collapse: false,
1053                            trim: false,
1054                        }
1055                    } else {
1056                        WhitespaceMinificationMode {
1057                            collapse: default_collapse,
1058                            trim: default_trim,
1059                        }
1060                    }
1061                }
1062            },
1063            Namespace::SVG => match &*element.tag_name {
1064                "script" | "style" => WhitespaceMinificationMode {
1065                    collapse: false,
1066                    trim: true,
1067                },
1068                // https://svgwg.org/svg2-draft/render.html#Definitions
1069                _ if matches!(
1070                    &*element.tag_name,
1071                    "a" | "circle"
1072                        | "ellipse"
1073                        | "foreignObject"
1074                        | "g"
1075                        | "image"
1076                        | "line"
1077                        | "path"
1078                        | "polygon"
1079                        | "polyline"
1080                        | "rect"
1081                        | "svg"
1082                        | "switch"
1083                        | "symbol"
1084                        | "text"
1085                        | "textPath"
1086                        | "tspan"
1087                        | "use"
1088                ) =>
1089                {
1090                    WhitespaceMinificationMode {
1091                        collapse: default_collapse,
1092                        trim: default_trim,
1093                    }
1094                }
1095                _ => WhitespaceMinificationMode {
1096                    collapse: default_collapse,
1097                    trim: !matches!(
1098                        self.options.collapse_whitespaces,
1099                        CollapseWhitespaces::None | CollapseWhitespaces::OnlyMetadata
1100                    ),
1101                },
1102            },
1103            _ => WhitespaceMinificationMode {
1104                collapse: false,
1105                trim: default_trim,
1106            },
1107        }
1108    }
1109
1110    fn collapse_whitespace<'a>(&self, data: &'a str) -> Cow<'a, str> {
1111        if data.is_empty() {
1112            return Cow::Borrowed(data);
1113        }
1114
1115        if data.chars().all(|c| !matches!(c, c if is_whitespace(c))) {
1116            return Cow::Borrowed(data);
1117        }
1118
1119        let mut collapsed = String::with_capacity(data.len());
1120        let mut in_whitespace = false;
1121
1122        for c in data.chars() {
1123            if is_whitespace(c) {
1124                if in_whitespace {
1125                    // Skip this character.
1126                    continue;
1127                };
1128
1129                in_whitespace = true;
1130
1131                collapsed.push(' ');
1132            } else {
1133                in_whitespace = false;
1134
1135                collapsed.push(c);
1136            };
1137        }
1138
1139        Cow::Owned(collapsed)
1140    }
1141
1142    fn is_additional_minifier_attribute(&self, name: &Atom) -> Option<MinifierType> {
1143        if let Some(minify_additional_attributes) = &self.options.minify_additional_attributes {
1144            for item in minify_additional_attributes {
1145                if item.0.is_match(name) {
1146                    return Some(item.1.clone());
1147                }
1148            }
1149        }
1150
1151        None
1152    }
1153
1154    fn is_empty_metadata_element(&self, child: &Child) -> bool {
1155        if let Child::Element(element) = child {
1156            if matches!(element.namespace, Namespace::HTML | Namespace::SVG)
1157                && element.tag_name == "style"
1158                && self.is_empty_children(&element.children)
1159            {
1160                if element.attributes.is_empty() {
1161                    return true;
1162                }
1163
1164                if element.attributes.len() == 1 {
1165                    return element.attributes.iter().all(|attr| {
1166                        attr.name == "type"
1167                            && attr.value.is_some()
1168                            && self.is_type_text_css(attr.value.as_ref().unwrap())
1169                    });
1170                }
1171            } else if matches!(element.namespace, Namespace::HTML | Namespace::SVG)
1172                && element.tag_name == "script"
1173                && self.is_empty_children(&element.children)
1174            {
1175                if element.attributes.is_empty() {
1176                    return true;
1177                }
1178
1179                if element.attributes.len() == 1 {
1180                    return element.attributes.iter().all(|attr| {
1181                        attr.name == "type"
1182                            && attr.value.is_some()
1183                            && (attr.value.as_deref() == Some("module")
1184                                || self.is_type_text_javascript(attr.value.as_ref().unwrap()))
1185                    });
1186                }
1187            } else if (!self.is_element_displayed(element)
1188                || (element.namespace == Namespace::HTML && element.tag_name == "noscript"))
1189                && element.attributes.is_empty()
1190                && self.is_empty_children(&element.children)
1191                && element.content.is_none()
1192            {
1193                return true;
1194            }
1195        }
1196
1197        false
1198    }
1199
1200    fn is_empty_children(&self, children: &Vec<Child>) -> bool {
1201        for child in children {
1202            match child {
1203                Child::Text(text) if text.data.chars().all(is_whitespace) => {
1204                    continue;
1205                }
1206                _ => return false,
1207            }
1208        }
1209
1210        true
1211    }
1212
1213    fn allow_elements_to_merge(&self, left: Option<&Child>, right: &Element) -> bool {
1214        if let Some(Child::Element(left)) = left {
1215            let is_style_tag = matches!(left.namespace, Namespace::HTML | Namespace::SVG)
1216                && left.tag_name == "style"
1217                && matches!(right.namespace, Namespace::HTML | Namespace::SVG)
1218                && right.tag_name == "style";
1219            let is_script_tag = matches!(left.namespace, Namespace::HTML | Namespace::SVG)
1220                && left.tag_name == "script"
1221                && matches!(right.namespace, Namespace::HTML | Namespace::SVG)
1222                && right.tag_name == "script";
1223
1224            if is_style_tag || is_script_tag {
1225                let mut need_skip = false;
1226
1227                let mut left_attributes = left
1228                    .attributes
1229                    .clone()
1230                    .into_iter()
1231                    .filter(|attribute| match &*attribute.name {
1232                        "src" if is_script_tag => {
1233                            need_skip = true;
1234
1235                            true
1236                        }
1237                        "type" => {
1238                            if let Some(value) = &attribute.value {
1239                                if (is_style_tag && self.is_type_text_css(value))
1240                                    || (is_script_tag && self.is_type_text_javascript(value))
1241                                {
1242                                    false
1243                                } else if is_script_tag
1244                                    && value.trim().eq_ignore_ascii_case("module")
1245                                {
1246                                    true
1247                                } else {
1248                                    need_skip = true;
1249
1250                                    true
1251                                }
1252                            } else {
1253                                true
1254                            }
1255                        }
1256                        _ => !self.is_default_attribute_value(left, attribute),
1257                    })
1258                    .map(|mut attribute| {
1259                        self.minify_attribute(left, &mut attribute);
1260
1261                        attribute
1262                    })
1263                    .collect::<Vec<Attribute>>();
1264
1265                if need_skip {
1266                    return false;
1267                }
1268
1269                let mut right_attributes = right
1270                    .attributes
1271                    .clone()
1272                    .into_iter()
1273                    .filter(|attribute| match &*attribute.name {
1274                        "src" if is_script_tag => {
1275                            need_skip = true;
1276
1277                            true
1278                        }
1279                        "type" => {
1280                            if let Some(value) = &attribute.value {
1281                                if (is_style_tag && self.is_type_text_css(value))
1282                                    || (is_script_tag && self.is_type_text_javascript(value))
1283                                {
1284                                    false
1285                                } else if is_script_tag
1286                                    && value.trim().eq_ignore_ascii_case("module")
1287                                {
1288                                    true
1289                                } else {
1290                                    need_skip = true;
1291
1292                                    true
1293                                }
1294                            } else {
1295                                true
1296                            }
1297                        }
1298                        _ => !self.is_default_attribute_value(right, attribute),
1299                    })
1300                    .map(|mut attribute| {
1301                        self.minify_attribute(right, &mut attribute);
1302
1303                        attribute
1304                    })
1305                    .collect::<Vec<Attribute>>();
1306
1307                if need_skip {
1308                    return false;
1309                }
1310
1311                left_attributes.sort_by(|a, b| a.name.cmp(&b.name));
1312                right_attributes.sort_by(|a, b| a.name.cmp(&b.name));
1313
1314                return left_attributes.eq_ignore_span(&right_attributes);
1315            }
1316        }
1317
1318        false
1319    }
1320
1321    fn merge_text_children(&self, left: &Element, right: &Element) -> Option<Vec<Child>> {
1322        let is_script_tag = matches!(left.namespace, Namespace::HTML | Namespace::SVG)
1323            && left.tag_name == "script"
1324            && matches!(right.namespace, Namespace::HTML | Namespace::SVG)
1325            && right.tag_name == "script";
1326
1327        // `script`/`style` elements should have only one text child
1328        let left_data = match left.children.first() {
1329            Some(Child::Text(left)) => left.data.to_string(),
1330            None => String::new(),
1331            _ => return None,
1332        };
1333
1334        let right_data = match right.children.first() {
1335            Some(Child::Text(right)) => right.data.to_string(),
1336            None => String::new(),
1337            _ => return None,
1338        };
1339
1340        let mut data = String::with_capacity(left_data.len() + right_data.len());
1341
1342        if is_script_tag {
1343            let is_modules = if is_script_tag {
1344                left.attributes.iter().any(|attribute| matches!(&attribute.value, Some(value) if value.trim().eq_ignore_ascii_case("module")))
1345            } else {
1346                false
1347            };
1348
1349            match self.merge_js(left_data, right_data, is_modules) {
1350                Some(minified) => {
1351                    data.push_str(&minified);
1352                }
1353                _ => {
1354                    return None;
1355                }
1356            }
1357        } else {
1358            data.push_str(&left_data);
1359            data.push_str(&right_data);
1360        }
1361
1362        if data.is_empty() {
1363            return Some(Vec::new());
1364        }
1365
1366        Some(vec![Child::Text(Text {
1367            span: DUMMY_SP,
1368            data: data.into(),
1369            raw: None,
1370        })])
1371    }
1372
1373    fn minify_children(&mut self, children: &mut Vec<Child>) -> Vec<Child> {
1374        if children.is_empty() {
1375            return Vec::new();
1376        }
1377
1378        let parent = match &self.current_element {
1379            Some(element) => element,
1380            _ => return children.to_vec(),
1381        };
1382
1383        let mode = self.get_whitespace_minification_for_tag(parent);
1384
1385        let child_will_be_retained =
1386            |child: &mut Child, prev_children: &mut Vec<Child>, next_children: &mut Vec<Child>| {
1387                match child {
1388                    Child::Comment(comment) if self.options.remove_comments => {
1389                        self.is_preserved_comment(&comment.data)
1390                    }
1391                    Child::Element(element)
1392                        if self.options.merge_metadata_elements
1393                            && self.allow_elements_to_merge(prev_children.last(), element) =>
1394                    {
1395                        if let Some(Child::Element(prev)) = prev_children.last_mut() {
1396                            if let Some(children) = self.merge_text_children(prev, element) {
1397                                prev.children = children;
1398
1399                                false
1400                            } else {
1401                                true
1402                            }
1403                        } else {
1404                            true
1405                        }
1406                    }
1407                    Child::Text(text) if text.data.is_empty() => false,
1408                    Child::Text(text)
1409                        if self.need_collapse_whitespace()
1410                            && parent.namespace == Namespace::HTML
1411                            && matches!(&*parent.tag_name, "html" | "head")
1412                            && text.data.chars().all(is_whitespace) =>
1413                    {
1414                        false
1415                    }
1416                    Child::Text(text)
1417                        if !self.descendant_of_pre
1418                            && get_white_space(parent.namespace, &parent.tag_name)
1419                                == WhiteSpace::Normal
1420                            && matches!(
1421                                self.options.collapse_whitespaces,
1422                                CollapseWhitespaces::All
1423                                    | CollapseWhitespaces::Smart
1424                                    | CollapseWhitespaces::OnlyMetadata
1425                                    | CollapseWhitespaces::Conservative
1426                                    | CollapseWhitespaces::AdvancedConservative
1427                            ) =>
1428                    {
1429                        let mut is_smart_left_trim = false;
1430                        let mut is_smart_right_trim = false;
1431
1432                        if matches!(
1433                            self.options.collapse_whitespaces,
1434                            CollapseWhitespaces::Smart
1435                                | CollapseWhitespaces::OnlyMetadata
1436                                | CollapseWhitespaces::AdvancedConservative
1437                        ) {
1438                            let need_remove_metadata_whitespaces = matches!(
1439                                self.options.collapse_whitespaces,
1440                                CollapseWhitespaces::OnlyMetadata
1441                                    | CollapseWhitespaces::AdvancedConservative
1442                            );
1443
1444                            let prev = prev_children.last();
1445                            let prev_display = match prev {
1446                                Some(Child::Element(element)) => Some(self.get_display(element)),
1447                                Some(Child::Comment(_)) => match need_remove_metadata_whitespaces {
1448                                    true => None,
1449                                    _ => Some(Display::None),
1450                                },
1451                                _ => None,
1452                            };
1453
1454                            let allow_to_trim_left = match prev_display {
1455                                // Block-level containers:
1456                                //
1457                                // `Display::Block`    - `display: block flow`
1458                                // `Display::ListItem` - `display: block flow list-item`
1459                                // `Display::Table`    - `display: block table`
1460                                //
1461                                // + internal table display (only whitespace characters allowed
1462                                // there)
1463                                Some(
1464                                    Display::Block
1465                                    | Display::ListItem
1466                                    | Display::Table
1467                                    | Display::TableColumnGroup
1468                                    | Display::TableCaption
1469                                    | Display::TableColumn
1470                                    | Display::TableRow
1471                                    | Display::TableCell
1472                                    | Display::TableHeaderGroup
1473                                    | Display::TableRowGroup
1474                                    | Display::TableFooterGroup,
1475                                ) => true,
1476                                // Elements are not displayed
1477                                // And
1478                                // Inline box
1479                                Some(Display::None) | Some(Display::Inline) => {
1480                                    // A custom element can contain any elements, we cannot predict
1481                                    // the behavior of spaces
1482                                    let is_custom_element =
1483                                        if let Some(Child::Element(element)) = &prev {
1484                                            self.is_custom_element(element)
1485                                        } else {
1486                                            false
1487                                        };
1488
1489                                    if is_custom_element {
1490                                        false
1491                                    } else {
1492                                        match &self.get_prev_displayed_node(
1493                                            prev_children,
1494                                            prev_children.len() - 1,
1495                                        ) {
1496                                            Some(Child::Text(text)) => {
1497                                                text.data.ends_with(is_whitespace)
1498                                            }
1499                                            Some(Child::Element(element)) => {
1500                                                let deep = if !element.children.is_empty() {
1501                                                    self.get_last_displayed_text_node(
1502                                                        &element.children,
1503                                                        element.children.len() - 1,
1504                                                    )
1505                                                } else {
1506                                                    None
1507                                                };
1508
1509                                                if let Some(deep) = deep {
1510                                                    deep.data.ends_with(is_whitespace)
1511                                                } else {
1512                                                    false
1513                                                }
1514                                            }
1515                                            _ => {
1516                                                let parent_display = self.get_display(parent);
1517
1518                                                match parent_display {
1519                                                    Display::Inline => {
1520                                                        if let Some(Child::Text(Text {
1521                                                            data,
1522                                                            ..
1523                                                        })) = &self.latest_element
1524                                                        {
1525                                                            data.ends_with(is_whitespace)
1526                                                        } else {
1527                                                            false
1528                                                        }
1529                                                    }
1530                                                    _ => true,
1531                                                }
1532                                            }
1533                                        }
1534                                    }
1535                                }
1536                                // Inline level containers and etc
1537                                Some(_) => false,
1538                                None => {
1539                                    // Template can be used in any place, so let's keep whitespaces
1540                                    //
1541                                    // For custom elements - an unnamed `<slot>` will be filled with
1542                                    // all of the custom
1543                                    // element's top-level child
1544                                    // nodes that do not have the slot
1545                                    // attribute. This includes text nodes.
1546                                    // Also they can be used for custom logic
1547
1548                                    if (parent.namespace == Namespace::HTML
1549                                        && parent.tag_name == "template")
1550                                        || self.is_custom_element(parent)
1551                                    {
1552                                        false
1553                                    } else {
1554                                        let parent_display = self.get_display(parent);
1555
1556                                        match parent_display {
1557                                            Display::Inline => {
1558                                                if let Some(Child::Text(Text { data, .. })) =
1559                                                    &self.latest_element
1560                                                {
1561                                                    data.ends_with(is_whitespace)
1562                                                } else {
1563                                                    false
1564                                                }
1565                                            }
1566                                            _ => true,
1567                                        }
1568                                    }
1569                                }
1570                            };
1571
1572                            let next = next_children.first();
1573                            let next_display = match next {
1574                                Some(Child::Element(element)) => Some(self.get_display(element)),
1575                                Some(Child::Comment(_)) => match need_remove_metadata_whitespaces {
1576                                    true => None,
1577                                    _ => Some(Display::None),
1578                                },
1579                                _ => None,
1580                            };
1581
1582                            let allow_to_trim_right = match next_display {
1583                                // Block-level containers:
1584                                //
1585                                // `Display::Block`    - `display: block flow`
1586                                // `Display::ListItem` - `display: block flow list-item`
1587                                // `Display::Table`    - `display: block table`
1588                                //
1589                                // + internal table display (only whitespace characters allowed
1590                                // there)
1591                                Some(
1592                                    Display::Block
1593                                    | Display::ListItem
1594                                    | Display::Table
1595                                    | Display::TableColumnGroup
1596                                    | Display::TableCaption
1597                                    | Display::TableColumn
1598                                    | Display::TableRow
1599                                    | Display::TableCell
1600                                    | Display::TableHeaderGroup
1601                                    | Display::TableRowGroup
1602                                    | Display::TableFooterGroup,
1603                                ) => true,
1604                                // These elements are not displayed
1605                                Some(Display::None) => {
1606                                    match &self.get_next_displayed_node(next_children, 0) {
1607                                        Some(Child::Text(text)) => {
1608                                            text.data.starts_with(is_whitespace)
1609                                        }
1610                                        Some(Child::Element(element)) => {
1611                                            let deep = self.get_first_displayed_text_node(
1612                                                &element.children,
1613                                                0,
1614                                            );
1615
1616                                            if let Some(deep) = deep {
1617                                                !deep.data.starts_with(is_whitespace)
1618                                            } else {
1619                                                false
1620                                            }
1621                                        }
1622                                        _ => {
1623                                            let parent_display = self.get_display(parent);
1624
1625                                            !matches!(parent_display, Display::Inline)
1626                                        }
1627                                    }
1628                                }
1629                                Some(_) => false,
1630                                None => {
1631                                    // Template can be used in any place, so let's keep whitespaces
1632                                    let is_template = parent.namespace == Namespace::HTML
1633                                        && parent.tag_name == "template";
1634
1635                                    if is_template {
1636                                        false
1637                                    } else {
1638                                        let parent_display = self.get_display(parent);
1639
1640                                        !matches!(parent_display, Display::Inline)
1641                                    }
1642                                }
1643                            };
1644
1645                            if matches!(
1646                                self.options.collapse_whitespaces,
1647                                CollapseWhitespaces::Smart
1648                            ) || (need_remove_metadata_whitespaces
1649                                && (prev_display == Some(Display::None)
1650                                    && next_display == Some(Display::None)))
1651                            {
1652                                is_smart_left_trim = allow_to_trim_left;
1653                                is_smart_right_trim = allow_to_trim_right;
1654                            }
1655                        }
1656
1657                        let mut value = if (mode.trim) || is_smart_left_trim {
1658                            text.data.trim_start_matches(is_whitespace)
1659                        } else {
1660                            &*text.data
1661                        };
1662
1663                        value = if (mode.trim) || is_smart_right_trim {
1664                            value.trim_end_matches(is_whitespace)
1665                        } else {
1666                            value
1667                        };
1668
1669                        if value.is_empty() {
1670                            false
1671                        } else if mode.collapse {
1672                            text.data = self.collapse_whitespace(value).into();
1673
1674                            true
1675                        } else {
1676                            text.data = value.into();
1677
1678                            true
1679                        }
1680                    }
1681                    _ => true,
1682                }
1683            };
1684
1685        let mut new_children = Vec::with_capacity(children.len());
1686
1687        for _ in 0..children.len() {
1688            let mut child = children.remove(0);
1689
1690            if let Child::Text(text) = &mut child {
1691                if let Some(Child::Text(prev_text)) = new_children.last_mut() {
1692                    let mut new_data =
1693                        String::with_capacity(prev_text.data.len() + text.data.len());
1694
1695                    new_data.push_str(&prev_text.data);
1696                    new_data.push_str(&text.data);
1697
1698                    text.data = new_data.into();
1699
1700                    new_children.pop();
1701                }
1702            };
1703
1704            let result = child_will_be_retained(&mut child, &mut new_children, children);
1705
1706            if self.options.remove_empty_metadata_elements {
1707                if let Some(last_child @ Child::Element(_)) = new_children.last() {
1708                    if self.is_empty_metadata_element(last_child) {
1709                        new_children.pop();
1710
1711                        if let Child::Text(text) = &mut child {
1712                            if let Some(Child::Text(prev_text)) = new_children.last_mut() {
1713                                let mut new_data =
1714                                    String::with_capacity(prev_text.data.len() + text.data.len());
1715
1716                                new_data.push_str(&prev_text.data);
1717                                new_data.push_str(&text.data);
1718
1719                                text.data = new_data.into();
1720
1721                                new_children.pop();
1722                            }
1723                        }
1724                    }
1725                }
1726            }
1727
1728            if result {
1729                new_children.push(child);
1730            }
1731        }
1732
1733        new_children
1734    }
1735
1736    fn get_attribute_value<'a>(
1737        &self,
1738        attributes: &'a Vec<Attribute>,
1739        name: &str,
1740    ) -> Option<&'a Atom> {
1741        let mut type_attribute_value = None;
1742
1743        for attribute in attributes {
1744            if attribute.name == name {
1745                if let Some(value) = &attribute.value {
1746                    type_attribute_value = Some(value);
1747                }
1748
1749                break;
1750            }
1751        }
1752
1753        type_attribute_value
1754    }
1755
1756    fn is_additional_scripts_content(&self, name: &str) -> Option<MinifierType> {
1757        if let Some(minify_additional_scripts_content) =
1758            &self.options.minify_additional_scripts_content
1759        {
1760            for item in minify_additional_scripts_content {
1761                if item.0.is_match(name) {
1762                    return Some(item.1.clone());
1763                }
1764            }
1765        }
1766
1767        None
1768    }
1769
1770    fn need_minify_json(&self) -> bool {
1771        match self.options.minify_json {
1772            MinifyJsonOption::Bool(value) => value,
1773            MinifyJsonOption::Options(_) => true,
1774        }
1775    }
1776
1777    fn get_json_options(&self) -> JsonOptions {
1778        match &self.options.minify_json {
1779            MinifyJsonOption::Bool(_) => JsonOptions { pretty: false },
1780            MinifyJsonOption::Options(json_options) => *json_options.clone(),
1781        }
1782    }
1783
1784    fn minify_json(&self, data: String) -> Option<String> {
1785        let json = match serde_json::from_str::<Value>(&data) {
1786            Ok(json) => json,
1787            _ => return None,
1788        };
1789
1790        let options = self.get_json_options();
1791        let result = match options.pretty {
1792            true => serde_json::to_string_pretty(&json),
1793            false => serde_json::to_string(&json),
1794        };
1795
1796        result.ok()
1797    }
1798
1799    fn need_minify_js(&self) -> bool {
1800        match self.options.minify_js {
1801            MinifyJsOption::Bool(value) => value,
1802            MinifyJsOption::Options(_) => true,
1803        }
1804    }
1805
1806    fn get_js_options(&self) -> JsOptions {
1807        match &self.options.minify_js {
1808            MinifyJsOption::Bool(_) => JsOptions {
1809                parser: JsParserOptions::default(),
1810                minifier: swc_ecma_minifier::option::MinifyOptions {
1811                    compress: Some(swc_ecma_minifier::option::CompressOptions::default()),
1812                    mangle: Some(swc_ecma_minifier::option::MangleOptions::default()),
1813                    ..Default::default()
1814                },
1815                codegen: swc_ecma_codegen::Config::default(),
1816            },
1817            MinifyJsOption::Options(js_options) => *js_options.clone(),
1818        }
1819    }
1820
1821    fn merge_js(&self, left: String, right: String, is_modules: bool) -> Option<String> {
1822        let comments = SingleThreadedComments::default();
1823        let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
1824
1825        // Left
1826        let mut left_errors: Vec<_> = Vec::new();
1827        let left_fm = cm.new_source_file(FileName::Anon.into(), left);
1828        let syntax = swc_ecma_parser::Syntax::default();
1829        // Use the latest target for merging
1830        let target = swc_ecma_ast::EsVersion::latest();
1831
1832        let mut left_program = if is_modules {
1833            match swc_ecma_parser::parse_file_as_module(
1834                &left_fm,
1835                syntax,
1836                target,
1837                Some(&comments),
1838                &mut left_errors,
1839            ) {
1840                Ok(module) => swc_ecma_ast::Program::Module(module),
1841                _ => return None,
1842            }
1843        } else {
1844            match swc_ecma_parser::parse_file_as_script(
1845                &left_fm,
1846                syntax,
1847                target,
1848                Some(&comments),
1849                &mut left_errors,
1850            ) {
1851                Ok(script) => swc_ecma_ast::Program::Script(script),
1852                _ => return None,
1853            }
1854        };
1855
1856        // Avoid compress potential invalid JS
1857        if !left_errors.is_empty() {
1858            return None;
1859        }
1860
1861        let unresolved_mark = Mark::new();
1862        let left_top_level_mark = Mark::new();
1863
1864        swc_ecma_visit::VisitMutWith::visit_mut_with(
1865            &mut left_program,
1866            &mut swc_ecma_transforms_base::resolver(unresolved_mark, left_top_level_mark, false),
1867        );
1868
1869        // Right
1870        let mut right_errors: Vec<_> = Vec::new();
1871        let right_fm = cm.new_source_file(FileName::Anon.into(), right);
1872
1873        let mut right_program = if is_modules {
1874            match swc_ecma_parser::parse_file_as_module(
1875                &right_fm,
1876                syntax,
1877                target,
1878                Some(&comments),
1879                &mut right_errors,
1880            ) {
1881                Ok(module) => swc_ecma_ast::Program::Module(module),
1882                _ => return None,
1883            }
1884        } else {
1885            match swc_ecma_parser::parse_file_as_script(
1886                &right_fm,
1887                syntax,
1888                target,
1889                Some(&comments),
1890                &mut right_errors,
1891            ) {
1892                Ok(script) => swc_ecma_ast::Program::Script(script),
1893                _ => return None,
1894            }
1895        };
1896
1897        // Avoid compress potential invalid JS
1898        if !right_errors.is_empty() {
1899            return None;
1900        }
1901
1902        let right_top_level_mark = Mark::new();
1903
1904        swc_ecma_visit::VisitMutWith::visit_mut_with(
1905            &mut right_program,
1906            &mut swc_ecma_transforms_base::resolver(unresolved_mark, right_top_level_mark, false),
1907        );
1908
1909        // Merge
1910        match &mut left_program {
1911            swc_ecma_ast::Program::Module(left_program) => match right_program {
1912                swc_ecma_ast::Program::Module(right_program) => {
1913                    left_program.body.extend(right_program.body);
1914                }
1915                _ => {
1916                    unreachable!();
1917                }
1918            },
1919            swc_ecma_ast::Program::Script(left_program) => match right_program {
1920                swc_ecma_ast::Program::Script(right_program) => {
1921                    left_program.body.extend(right_program.body);
1922                }
1923                _ => {
1924                    unreachable!();
1925                }
1926            },
1927            #[cfg(swc_ast_unknown)]
1928            _ => panic!("unable to access unknown nodes"),
1929        }
1930
1931        if is_modules {
1932            swc_ecma_visit::VisitMutWith::visit_mut_with(
1933                &mut left_program,
1934                &mut swc_ecma_transforms_base::hygiene::hygiene(),
1935            );
1936        }
1937
1938        let left_program =
1939            left_program.apply(swc_ecma_transforms_base::fixer::fixer(Some(&comments)));
1940
1941        let mut buf = Vec::new();
1942
1943        {
1944            let wr = Box::new(swc_ecma_codegen::text_writer::JsWriter::new(
1945                cm.clone(),
1946                "\n",
1947                &mut buf,
1948                None,
1949            )) as Box<dyn swc_ecma_codegen::text_writer::WriteJs>;
1950
1951            let mut emitter = swc_ecma_codegen::Emitter {
1952                cfg: swc_ecma_codegen::Config::default().with_target(target),
1953                cm,
1954                comments: Some(&comments),
1955                wr,
1956            };
1957
1958            emitter.emit_program(&left_program).unwrap();
1959        }
1960
1961        let code = match String::from_utf8(buf) {
1962            Ok(minified) => minified,
1963            _ => return None,
1964        };
1965
1966        Some(code)
1967    }
1968
1969    // TODO source map url output for JS and CSS?
1970    fn minify_js(&self, data: String, is_module: bool, is_attribute: bool) -> Option<String> {
1971        let mut errors: Vec<_> = Vec::new();
1972
1973        let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
1974        let fm = cm.new_source_file(FileName::Anon.into(), data);
1975        let mut options = self.get_js_options();
1976
1977        if let swc_ecma_parser::Syntax::Es(es_config) = &mut options.parser.syntax {
1978            es_config.allow_return_outside_function = !is_module && is_attribute;
1979        }
1980
1981        let comments = SingleThreadedComments::default();
1982
1983        let mut program = if is_module {
1984            match swc_ecma_parser::parse_file_as_module(
1985                &fm,
1986                options.parser.syntax,
1987                options.parser.target,
1988                if options.parser.comments {
1989                    Some(&comments)
1990                } else {
1991                    None
1992                },
1993                &mut errors,
1994            ) {
1995                Ok(module) => swc_ecma_ast::Program::Module(module),
1996                _ => return None,
1997            }
1998        } else {
1999            match swc_ecma_parser::parse_file_as_script(
2000                &fm,
2001                options.parser.syntax,
2002                options.parser.target,
2003                if options.parser.comments {
2004                    Some(&comments)
2005                } else {
2006                    None
2007                },
2008                &mut errors,
2009            ) {
2010                Ok(script) => swc_ecma_ast::Program::Script(script),
2011                _ => return None,
2012            }
2013        };
2014
2015        // Avoid compress potential invalid JS
2016        if !errors.is_empty() {
2017            return None;
2018        }
2019
2020        if let Some(compress_options) = &mut options.minifier.compress {
2021            compress_options.module = is_module;
2022        } else {
2023            options.minifier.compress = Some(swc_ecma_minifier::option::CompressOptions {
2024                ecma: options.parser.target,
2025                ..Default::default()
2026            });
2027        }
2028
2029        let unresolved_mark = Mark::new();
2030        let top_level_mark = Mark::new();
2031
2032        swc_ecma_visit::VisitMutWith::visit_mut_with(
2033            &mut program,
2034            &mut swc_ecma_transforms_base::resolver(unresolved_mark, top_level_mark, false),
2035        );
2036
2037        let program = program.apply(swc_ecma_transforms_base::fixer::paren_remover(Some(
2038            &comments,
2039        )));
2040
2041        let program = swc_ecma_minifier::optimize(
2042            program,
2043            cm.clone(),
2044            if options.parser.comments {
2045                Some(&comments)
2046            } else {
2047                None
2048            },
2049            None,
2050            // TODO allow to keep `var`/function/etc on top level
2051            &options.minifier,
2052            &swc_ecma_minifier::option::ExtraOptions {
2053                unresolved_mark,
2054                top_level_mark,
2055                mangle_name_cache: None,
2056            },
2057        );
2058
2059        let program = program.apply(swc_ecma_transforms_base::fixer::fixer(Some(&comments)));
2060
2061        let mut buf = Vec::new();
2062
2063        {
2064            let mut wr = Box::new(swc_ecma_codegen::text_writer::JsWriter::new(
2065                cm.clone(),
2066                "\n",
2067                &mut buf,
2068                None,
2069            )) as Box<dyn swc_ecma_codegen::text_writer::WriteJs>;
2070
2071            wr = Box::new(swc_ecma_codegen::text_writer::omit_trailing_semi(wr));
2072
2073            options.codegen.minify = true;
2074            options.codegen.target = options.parser.target;
2075            options.codegen.omit_last_semi = true;
2076
2077            let mut emitter = swc_ecma_codegen::Emitter {
2078                cfg: options.codegen,
2079                cm,
2080                comments: if options.parser.comments {
2081                    Some(&comments)
2082                } else {
2083                    None
2084                },
2085                wr,
2086            };
2087
2088            emitter.emit_program(&program).unwrap();
2089        }
2090
2091        let minified = match String::from_utf8(buf) {
2092            // Avoid generating the sequence "</script" in JS code
2093            // TODO move it to ecma codegen under the option?
2094            Ok(minified) => minified.replace("</script>", "<\\/script>"),
2095            _ => return None,
2096        };
2097
2098        Some(minified)
2099    }
2100
2101    fn need_minify_css(&self) -> bool {
2102        match self.options.minify_css {
2103            MinifyCssOption::Bool(value) => value,
2104            MinifyCssOption::Options(_) => true,
2105        }
2106    }
2107
2108    fn minify_sizes(&self, value: &str) -> Option<String> {
2109        let values = value
2110            .rsplitn(2, ['\t', '\n', '\x0C', '\r', ' '])
2111            .collect::<Vec<&str>>();
2112
2113        if values.len() != 2 {
2114            return None;
2115        }
2116
2117        if !values[1].starts_with('(') {
2118            return None;
2119        }
2120
2121        let media_condition =
2122            // It should be `MediaCondition`, but `<media-query> = <media-condition>` and other values is just invalid size
2123            match self.minify_css(values[1].to_string(), CssMinificationMode::MediaQueryList) {
2124                Some(minified) => minified,
2125                _ => return None,
2126            };
2127
2128        let source_size_value = values[0];
2129        let mut minified = String::with_capacity(media_condition.len() + source_size_value.len());
2130
2131        minified.push_str(&media_condition);
2132        minified.push(' ');
2133        minified.push_str(source_size_value);
2134
2135        Some(minified)
2136    }
2137
2138    fn minify_css(&self, data: String, mode: CssMinificationMode) -> Option<String> {
2139        self.css_minifier
2140            .minify_css(&self.options.minify_css, data, mode)
2141    }
2142
2143    fn minify_html(&self, data: String, mode: HtmlMinificationMode) -> Option<String> {
2144        let mut errors: Vec<_> = Vec::new();
2145
2146        let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
2147        let fm = cm.new_source_file(FileName::Anon.into(), data);
2148
2149        // Emulate content inside conditional comments like content inside the
2150        // `template` element
2151        let mut context_element = None;
2152
2153        let mut document_or_document_fragment = match mode {
2154            HtmlMinificationMode::ConditionalComments => {
2155                // Emulate content inside conditional comments like content inside the
2156                // `template` element, because it can be used in any place in source code
2157                context_element = Some(Element {
2158                    span: Default::default(),
2159                    tag_name: atom!("template"),
2160                    namespace: Namespace::HTML,
2161                    attributes: Vec::new(),
2162                    children: Vec::new(),
2163                    content: None,
2164                    is_self_closing: false,
2165                });
2166
2167                match swc_html_parser::parse_file_as_document_fragment(
2168                    &fm,
2169                    context_element.as_ref().unwrap(),
2170                    DocumentMode::NoQuirks,
2171                    None,
2172                    Default::default(),
2173                    &mut errors,
2174                ) {
2175                    Ok(document_fragment) => HtmlRoot::DocumentFragment(document_fragment),
2176                    _ => return None,
2177                }
2178            }
2179            HtmlMinificationMode::DocumentIframeSrcdoc => {
2180                match swc_html_parser::parse_file_as_document(
2181                    &fm,
2182                    ParserConfig {
2183                        iframe_srcdoc: true,
2184                        ..Default::default()
2185                    },
2186                    &mut errors,
2187                ) {
2188                    Ok(document) => HtmlRoot::Document(document),
2189                    _ => return None,
2190                }
2191            }
2192        };
2193
2194        // Avoid compress potential invalid CSS
2195        if !errors.is_empty() {
2196            return None;
2197        }
2198
2199        match document_or_document_fragment {
2200            HtmlRoot::Document(ref mut document) => {
2201                minify_document_with_custom_css_minifier(document, self.options, self.css_minifier);
2202            }
2203            HtmlRoot::DocumentFragment(ref mut document_fragment) => {
2204                minify_document_fragment_with_custom_css_minifier(
2205                    document_fragment,
2206                    context_element.as_ref().unwrap(),
2207                    self.options,
2208                    self.css_minifier,
2209                )
2210            }
2211        }
2212
2213        let mut minified = String::new();
2214        let wr = swc_html_codegen::writer::basic::BasicHtmlWriter::new(
2215            &mut minified,
2216            None,
2217            swc_html_codegen::writer::basic::BasicHtmlWriterConfig::default(),
2218        );
2219        let mut gen = swc_html_codegen::CodeGenerator::new(
2220            wr,
2221            swc_html_codegen::CodegenConfig {
2222                minify: true,
2223                scripting_enabled: false,
2224                context_element: context_element.as_ref(),
2225                tag_omission: None,
2226                keep_head_and_body: None,
2227                self_closing_void_elements: None,
2228                quotes: None,
2229            },
2230        );
2231
2232        match document_or_document_fragment {
2233            HtmlRoot::Document(document) => {
2234                swc_html_codegen::Emit::emit(&mut gen, &document).unwrap();
2235            }
2236            HtmlRoot::DocumentFragment(document_fragment) => {
2237                swc_html_codegen::Emit::emit(&mut gen, &document_fragment).unwrap();
2238            }
2239        }
2240
2241        Some(minified)
2242    }
2243
2244    fn minify_attribute(&self, element: &Element, n: &mut Attribute) {
2245        if let Some(value) = &n.value {
2246            if value.is_empty() {
2247                if (self.options.collapse_boolean_attributes
2248                    && self.is_boolean_attribute(element, n))
2249                    || (self.options.normalize_attributes
2250                        && self.is_crossorigin_attribute(element, n)
2251                        && value.is_empty())
2252                {
2253                    n.value = None;
2254                }
2255
2256                return;
2257            }
2258
2259            match (element.namespace, &*element.tag_name, &*n.name) {
2260                (Namespace::HTML, "iframe", "srcdoc") => {
2261                    if let Some(minified) = self.minify_html(
2262                        value.to_string(),
2263                        HtmlMinificationMode::DocumentIframeSrcdoc,
2264                    ) {
2265                        n.value = Some(minified.into());
2266                    };
2267                }
2268                (
2269                    Namespace::HTML | Namespace::SVG,
2270                    "style" | "link" | "script" | "input",
2271                    "type",
2272                ) if self.options.normalize_attributes => {
2273                    n.value = Some(value.trim().to_ascii_lowercase().into());
2274                }
2275                _ if self.options.normalize_attributes
2276                    && self.is_crossorigin_attribute(element, n)
2277                    && value.to_ascii_lowercase() == "anonymous" =>
2278                {
2279                    n.value = None;
2280                }
2281                _ if self.options.collapse_boolean_attributes
2282                    && self.is_boolean_attribute(element, n) =>
2283                {
2284                    n.value = None;
2285                }
2286                _ if self.is_event_handler_attribute(n) => {
2287                    let mut value = value.to_string();
2288
2289                    if self.options.normalize_attributes {
2290                        value = value.trim().into();
2291
2292                        if value.trim().to_lowercase().starts_with("javascript:") {
2293                            value = value.chars().skip(11).collect();
2294                        }
2295                    }
2296
2297                    if self.need_minify_js() {
2298                        if let Some(minified) = self.minify_js(value, false, true) {
2299                            n.value = Some(minified.into());
2300                        };
2301                    } else {
2302                        n.value = Some(value.into());
2303                    }
2304                }
2305                _ if self.options.normalize_attributes
2306                    && element.namespace == Namespace::HTML
2307                    && n.name == "contenteditable"
2308                    && n.value.as_deref() == Some("true") =>
2309                {
2310                    n.value = Some(atom!(""));
2311                }
2312                _ if self.options.normalize_attributes
2313                    && self.is_semicolon_separated_attribute(element, n) =>
2314                {
2315                    n.value = Some(
2316                        value
2317                            .split(';')
2318                            .map(|value| self.collapse_whitespace(value.trim()))
2319                            .collect::<Vec<_>>()
2320                            .join(";")
2321                            .into(),
2322                    );
2323                }
2324                _ if self.options.normalize_attributes
2325                    && n.name == "content"
2326                    && self.element_has_attribute_with_value(
2327                        element,
2328                        "http-equiv",
2329                        &["content-security-policy"],
2330                    ) =>
2331                {
2332                    let mut new_values = Vec::new();
2333
2334                    for value in value.trim().split(';') {
2335                        new_values.push(
2336                            value
2337                                .trim()
2338                                .split(' ')
2339                                .filter(|s| !s.is_empty())
2340                                .collect::<Vec<_>>()
2341                                .join(" "),
2342                        );
2343                    }
2344
2345                    let mut value = new_values.join(";");
2346
2347                    if value.ends_with(';') {
2348                        value.pop();
2349                    }
2350
2351                    n.value = Some(value.into());
2352                }
2353                _ if self.options.sort_space_separated_attribute_values
2354                    && self.is_attribute_value_unordered_set(element, n) =>
2355                {
2356                    let mut values = value.split_whitespace().collect::<Vec<_>>();
2357
2358                    values.sort_unstable();
2359
2360                    n.value = Some(values.join(" ").into());
2361                }
2362                _ if self.options.normalize_attributes
2363                    && self.is_space_separated_attribute(element, n) =>
2364                {
2365                    n.value = Some(
2366                        value
2367                            .split_whitespace()
2368                            .collect::<Vec<_>>()
2369                            .join(" ")
2370                            .into(),
2371                    );
2372                }
2373                _ if self.is_comma_separated_attribute(element, n) => {
2374                    let mut value = value.to_string();
2375
2376                    if self.options.normalize_attributes {
2377                        value = value
2378                            .split(',')
2379                            .map(|value| {
2380                                if matches!(&*n.name, "sizes" | "imagesizes") {
2381                                    let trimmed = value.trim();
2382
2383                                    match self.minify_sizes(trimmed) {
2384                                        Some(minified) => minified,
2385                                        _ => trimmed.to_string(),
2386                                    }
2387                                } else if matches!(&*n.name, "points") {
2388                                    self.collapse_whitespace(value.trim()).to_string()
2389                                } else if matches!(&*n.name, "exportparts") {
2390                                    value.chars().filter(|c| !c.is_whitespace()).collect()
2391                                } else {
2392                                    value.trim().to_string()
2393                                }
2394                            })
2395                            .collect::<Vec<_>>()
2396                            .join(",");
2397                    }
2398
2399                    if self.need_minify_css() && n.name == "media" && !value.is_empty() {
2400                        if let Some(minified) =
2401                            self.minify_css(value, CssMinificationMode::MediaQueryList)
2402                        {
2403                            n.value = Some(minified.into());
2404                        }
2405                    } else {
2406                        n.value = Some(value.into());
2407                    }
2408                }
2409                _ if self.is_trimable_separated_attribute(element, n) => {
2410                    let mut value = value.to_string();
2411
2412                    let fallback = |n: &mut Attribute| {
2413                        if self.options.normalize_attributes {
2414                            n.value = Some(value.trim().into());
2415                        }
2416                    };
2417
2418                    if self.need_minify_css() && n.name == "style" && !value.is_empty() {
2419                        let value = value.trim();
2420
2421                        if let Some(minified) = self
2422                            .minify_css(value.to_string(), CssMinificationMode::ListOfDeclarations)
2423                        {
2424                            n.value = Some(minified.into());
2425                        } else {
2426                            fallback(n);
2427                        }
2428                    } else if self.need_minify_js() && self.is_javascript_url_element(element) {
2429                        if value.trim().to_lowercase().starts_with("javascript:") {
2430                            value = value.trim().chars().skip(11).collect();
2431
2432                            if let Some(minified) = self.minify_js(value, false, true) {
2433                                let mut with_javascript =
2434                                    String::with_capacity(11 + minified.len());
2435
2436                                with_javascript.push_str("javascript:");
2437                                with_javascript.push_str(&minified);
2438
2439                                n.value = Some(with_javascript.into());
2440                            }
2441                        } else {
2442                            fallback(n);
2443                        }
2444                    } else {
2445                        fallback(n);
2446                    }
2447                }
2448                _ if self.options.minify_additional_attributes.is_some() => {
2449                    match self.is_additional_minifier_attribute(&n.name) {
2450                        Some(MinifierType::JsScript) if self.need_minify_js() => {
2451                            if let Some(minified) = self.minify_js(value.to_string(), false, true) {
2452                                n.value = Some(minified.into());
2453                            }
2454                        }
2455                        Some(MinifierType::JsModule) if self.need_minify_js() => {
2456                            if let Some(minified) = self.minify_js(value.to_string(), true, true) {
2457                                n.value = Some(minified.into());
2458                            }
2459                        }
2460                        Some(MinifierType::Json) if self.need_minify_json() => {
2461                            if let Some(minified) = self.minify_json(value.to_string()) {
2462                                n.value = Some(minified.into());
2463                            }
2464                        }
2465                        Some(MinifierType::Css) if self.need_minify_css() => {
2466                            if let Some(minified) = self.minify_css(
2467                                value.to_string(),
2468                                CssMinificationMode::ListOfDeclarations,
2469                            ) {
2470                                n.value = Some(minified.into());
2471                            }
2472                        }
2473                        Some(MinifierType::Html) => {
2474                            if let Some(minified) = self.minify_html(
2475                                value.to_string(),
2476                                HtmlMinificationMode::DocumentIframeSrcdoc,
2477                            ) {
2478                                n.value = Some(minified.into());
2479                            };
2480                        }
2481                        _ => {}
2482                    }
2483                }
2484                _ => {}
2485            }
2486        }
2487    }
2488}
2489
2490impl<C: MinifyCss> VisitMut for Minifier<'_, C> {
2491    fn visit_mut_document(&mut self, n: &mut Document) {
2492        n.visit_mut_children_with(self);
2493
2494        n.children
2495            .retain(|child| !matches!(child, Child::Comment(_) if self.options.remove_comments));
2496    }
2497
2498    fn visit_mut_document_fragment(&mut self, n: &mut DocumentFragment) {
2499        n.children = self.minify_children(&mut n.children);
2500
2501        n.visit_mut_children_with(self);
2502    }
2503
2504    fn visit_mut_document_type(&mut self, n: &mut DocumentType) {
2505        n.visit_mut_children_with(self);
2506
2507        if !self.options.force_set_html5_doctype {
2508            return;
2509        }
2510
2511        n.name = Some(atom!("html"));
2512        n.system_id = None;
2513        n.public_id = None;
2514    }
2515
2516    fn visit_mut_child(&mut self, n: &mut Child) {
2517        n.visit_mut_children_with(self);
2518
2519        self.current_element = None;
2520
2521        if matches!(
2522            self.options.collapse_whitespaces,
2523            CollapseWhitespaces::Smart
2524                | CollapseWhitespaces::AdvancedConservative
2525                | CollapseWhitespaces::OnlyMetadata
2526        ) {
2527            match n {
2528                Child::Text(_) | Child::Element(_) => {
2529                    self.latest_element = Some(n.clone());
2530                }
2531                _ => {}
2532            }
2533        }
2534    }
2535
2536    fn visit_mut_element(&mut self, n: &mut Element) {
2537        // Don't copy children to save memory
2538        self.current_element = Some(Element {
2539            span: Default::default(),
2540            tag_name: n.tag_name.clone(),
2541            namespace: n.namespace,
2542            attributes: n.attributes.clone(),
2543            children: Vec::new(),
2544            content: None,
2545            is_self_closing: n.is_self_closing,
2546        });
2547
2548        let old_descendant_of_pre = self.descendant_of_pre;
2549
2550        if self.need_collapse_whitespace() && !old_descendant_of_pre {
2551            self.descendant_of_pre = get_white_space(n.namespace, &n.tag_name) == WhiteSpace::Pre;
2552        }
2553
2554        n.children = self.minify_children(&mut n.children);
2555
2556        n.visit_mut_children_with(self);
2557
2558        // Remove all leading and trailing whitespaces for the `body` element
2559        if n.namespace == Namespace::HTML && n.tag_name == "body" && self.need_collapse_whitespace()
2560        {
2561            self.remove_leading_and_trailing_whitespaces(&mut n.children, true, true);
2562        }
2563
2564        if self.need_collapse_whitespace() {
2565            self.descendant_of_pre = old_descendant_of_pre;
2566        }
2567
2568        let mut remove_list = Vec::new();
2569
2570        for (i, i1) in n.attributes.iter().enumerate() {
2571            if i1.value.is_some() {
2572                if self.options.remove_redundant_attributes != RemoveRedundantAttributes::None
2573                    && self.is_default_attribute_value(n, i1)
2574                {
2575                    remove_list.push(i);
2576
2577                    continue;
2578                }
2579
2580                if self.options.remove_empty_attributes {
2581                    let value = i1.value.as_ref().unwrap();
2582
2583                    if (matches!(&*i1.name, "id") && value.is_empty())
2584                        || (matches!(&*i1.name, "class" | "style") && value.is_empty())
2585                        || self.is_event_handler_attribute(i1) && value.is_empty()
2586                    {
2587                        remove_list.push(i);
2588
2589                        continue;
2590                    }
2591                }
2592            }
2593
2594            for (j, j1) in n.attributes.iter().enumerate() {
2595                if i < j && i1.name == j1.name {
2596                    remove_list.push(j);
2597                }
2598            }
2599        }
2600
2601        // Fast path. We don't face real duplicates in most cases.
2602        if !remove_list.is_empty() {
2603            let new = take(&mut n.attributes)
2604                .into_iter()
2605                .enumerate()
2606                .filter_map(|(idx, value)| {
2607                    if remove_list.contains(&idx) {
2608                        None
2609                    } else {
2610                        Some(value)
2611                    }
2612                })
2613                .collect::<Vec<_>>();
2614
2615            n.attributes = new;
2616        }
2617
2618        if let Some(attribute_name_counter) = &self.attribute_name_counter {
2619            n.attributes.sort_by(|a, b| {
2620                let ordeing = attribute_name_counter
2621                    .get(&b.name)
2622                    .cmp(&attribute_name_counter.get(&a.name));
2623
2624                match ordeing {
2625                    Ordering::Equal => b.name.cmp(&a.name),
2626                    _ => ordeing,
2627                }
2628            });
2629        }
2630    }
2631
2632    fn visit_mut_attribute(&mut self, n: &mut Attribute) {
2633        n.visit_mut_children_with(self);
2634
2635        let element = match &self.current_element {
2636            Some(current_element) => current_element,
2637            _ => return,
2638        };
2639
2640        self.minify_attribute(element, n);
2641    }
2642
2643    fn visit_mut_text(&mut self, n: &mut Text) {
2644        n.visit_mut_children_with(self);
2645
2646        if n.data.is_empty() {
2647            return;
2648        }
2649
2650        let mut text_type = None;
2651
2652        if let Some(current_element) = &self.current_element {
2653            match &*current_element.tag_name {
2654                "script"
2655                    if (self.need_minify_json() || self.need_minify_js())
2656                        && matches!(
2657                            current_element.namespace,
2658                            Namespace::HTML | Namespace::SVG
2659                        )
2660                        && !current_element
2661                            .attributes
2662                            .iter()
2663                            .any(|attribute| matches!(&*attribute.name, "src")) =>
2664                {
2665                    let type_attribute_value: Option<Atom> = self
2666                        .get_attribute_value(&current_element.attributes, "type")
2667                        .map(|v| v.to_ascii_lowercase().trim().into());
2668
2669                    match type_attribute_value.as_deref() {
2670                        Some("module") if self.need_minify_js() => {
2671                            text_type = Some(MinifierType::JsModule);
2672                        }
2673                        Some(value)
2674                            if self.need_minify_js() && self.is_type_text_javascript(value) =>
2675                        {
2676                            text_type = Some(MinifierType::JsScript);
2677                        }
2678                        None if self.need_minify_js() => {
2679                            text_type = Some(MinifierType::JsScript);
2680                        }
2681                        Some(
2682                            "application/json"
2683                            | "application/ld+json"
2684                            | "importmap"
2685                            | "speculationrules",
2686                        ) if self.need_minify_json() => {
2687                            text_type = Some(MinifierType::Json);
2688                        }
2689                        Some(script_type)
2690                            if self.options.minify_additional_scripts_content.is_some() =>
2691                        {
2692                            if let Some(minifier_type) =
2693                                self.is_additional_scripts_content(script_type)
2694                            {
2695                                text_type = Some(minifier_type);
2696                            }
2697                        }
2698                        _ => {}
2699                    }
2700                }
2701                "style"
2702                    if self.need_minify_css()
2703                        && matches!(
2704                            current_element.namespace,
2705                            Namespace::HTML | Namespace::SVG
2706                        ) =>
2707                {
2708                    let mut type_attribute_value = None;
2709
2710                    for attribute in &current_element.attributes {
2711                        if attribute.name == "type" && attribute.value.is_some() {
2712                            type_attribute_value = Some(attribute.value.as_ref().unwrap());
2713
2714                            break;
2715                        }
2716                    }
2717
2718                    if type_attribute_value.is_none()
2719                        || self.is_type_text_css(type_attribute_value.as_ref().unwrap())
2720                    {
2721                        text_type = Some(MinifierType::Css)
2722                    }
2723                }
2724                "title" if current_element.namespace == Namespace::HTML => {
2725                    n.data = self.collapse_whitespace(&n.data).trim().into();
2726                }
2727                _ => {}
2728            }
2729        }
2730
2731        match text_type {
2732            Some(MinifierType::JsScript) => {
2733                let minified = match self.minify_js(n.data.to_string(), false, false) {
2734                    Some(minified) => minified,
2735                    None => return,
2736                };
2737
2738                n.data = minified.into();
2739            }
2740            Some(MinifierType::JsModule) => {
2741                let minified = match self.minify_js(n.data.to_string(), true, false) {
2742                    Some(minified) => minified,
2743                    None => return,
2744                };
2745
2746                n.data = minified.into();
2747            }
2748            Some(MinifierType::Json) => {
2749                let minified = match self.minify_json(n.data.to_string()) {
2750                    Some(minified) => minified,
2751                    None => return,
2752                };
2753
2754                n.data = minified.into();
2755            }
2756            Some(MinifierType::Css) => {
2757                let minified =
2758                    match self.minify_css(n.data.to_string(), CssMinificationMode::Stylesheet) {
2759                        Some(minified) => minified,
2760                        None => return,
2761                    };
2762
2763                n.data = minified.into();
2764            }
2765            Some(MinifierType::Html) => {
2766                let minified = match self.minify_html(
2767                    n.data.to_string(),
2768                    HtmlMinificationMode::ConditionalComments,
2769                ) {
2770                    Some(minified) => minified,
2771                    None => return,
2772                };
2773
2774                n.data = minified.into();
2775            }
2776            _ => {}
2777        }
2778    }
2779
2780    fn visit_mut_comment(&mut self, n: &mut Comment) {
2781        n.visit_mut_children_with(self);
2782
2783        if !self.options.minify_conditional_comments {
2784            return;
2785        }
2786
2787        if self.is_conditional_comment(&n.data) && !n.data.is_empty() {
2788            let start_pos = match n.data.find("]>") {
2789                Some(start_pos) => start_pos,
2790                _ => return,
2791            };
2792            let end_pos = match n.data.find("<![") {
2793                Some(end_pos) => end_pos,
2794                _ => return,
2795            };
2796
2797            let html = n
2798                .data
2799                .chars()
2800                .skip(start_pos)
2801                .take(end_pos - start_pos)
2802                .collect();
2803
2804            let minified = match self.minify_html(html, HtmlMinificationMode::ConditionalComments) {
2805                Some(minified) => minified,
2806                _ => return,
2807            };
2808            let before: String = n.data.chars().take(start_pos).collect();
2809            let after: String = n.data.chars().skip(end_pos).take(n.data.len()).collect();
2810            let mut data = String::with_capacity(n.data.len());
2811
2812            data.push_str(&before);
2813            data.push_str(&minified);
2814            data.push_str(&after);
2815
2816            n.data = data.into();
2817        }
2818    }
2819}
2820
2821struct AttributeNameCounter {
2822    tree: FxHashMap<Atom, usize>,
2823}
2824
2825impl VisitMut for AttributeNameCounter {
2826    fn visit_mut_attribute(&mut self, n: &mut Attribute) {
2827        n.visit_mut_children_with(self);
2828
2829        *self.tree.entry(n.name.clone()).or_insert(0) += 1;
2830    }
2831}
2832
2833pub trait MinifyCss {
2834    type Options;
2835    fn minify_css(
2836        &self,
2837        options: &MinifyCssOption<Self::Options>,
2838        data: String,
2839        mode: CssMinificationMode,
2840    ) -> Option<String>;
2841}
2842
2843#[cfg(feature = "default-css-minifier")]
2844struct DefaultCssMinifier;
2845
2846#[cfg(feature = "default-css-minifier")]
2847impl DefaultCssMinifier {
2848    fn get_css_options(&self, options: &MinifyCssOption<CssOptions>) -> CssOptions {
2849        match options {
2850            MinifyCssOption::Bool(_) => CssOptions {
2851                parser: swc_css_parser::parser::ParserConfig::default(),
2852                minifier: swc_css_minifier::options::MinifyOptions::default(),
2853                codegen: swc_css_codegen::CodegenConfig::default(),
2854            },
2855            MinifyCssOption::Options(css_options) => css_options.clone(),
2856        }
2857    }
2858}
2859
2860#[cfg(feature = "default-css-minifier")]
2861impl MinifyCss for DefaultCssMinifier {
2862    type Options = CssOptions;
2863
2864    fn minify_css(
2865        &self,
2866        options: &MinifyCssOption<Self::Options>,
2867        data: String,
2868        mode: CssMinificationMode,
2869    ) -> Option<String> {
2870        let mut errors: Vec<_> = Vec::new();
2871
2872        let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
2873        let fm = cm.new_source_file(FileName::Anon.into(), data);
2874
2875        let mut options = self.get_css_options(options);
2876
2877        let mut stylesheet = match mode {
2878            CssMinificationMode::Stylesheet => {
2879                match swc_css_parser::parse_file(&fm, None, options.parser, &mut errors) {
2880                    Ok(stylesheet) => stylesheet,
2881                    _ => return None,
2882                }
2883            }
2884            CssMinificationMode::ListOfDeclarations => {
2885                match swc_css_parser::parse_file::<Vec<swc_css_ast::DeclarationOrAtRule>>(
2886                    &fm,
2887                    None,
2888                    options.parser,
2889                    &mut errors,
2890                ) {
2891                    Ok(list_of_declarations) => {
2892                        let declaration_list: Vec<swc_css_ast::ComponentValue> =
2893                            list_of_declarations
2894                                .into_iter()
2895                                .map(|node| node.into())
2896                                .collect();
2897
2898                        swc_css_ast::Stylesheet {
2899                            span: Default::default(),
2900                            rules: vec![swc_css_ast::Rule::QualifiedRule(
2901                                swc_css_ast::QualifiedRule {
2902                                    span: Default::default(),
2903                                    prelude: swc_css_ast::QualifiedRulePrelude::SelectorList(
2904                                        swc_css_ast::SelectorList {
2905                                            span: Default::default(),
2906                                            children: Vec::new(),
2907                                        },
2908                                    ),
2909                                    block: swc_css_ast::SimpleBlock {
2910                                        span: Default::default(),
2911                                        name: swc_css_ast::TokenAndSpan {
2912                                            span: DUMMY_SP,
2913                                            token: swc_css_ast::Token::LBrace,
2914                                        },
2915                                        value: declaration_list,
2916                                    },
2917                                }
2918                                .into(),
2919                            )],
2920                        }
2921                    }
2922                    _ => return None,
2923                }
2924            }
2925            CssMinificationMode::MediaQueryList => {
2926                match swc_css_parser::parse_file::<swc_css_ast::MediaQueryList>(
2927                    &fm,
2928                    None,
2929                    options.parser,
2930                    &mut errors,
2931                ) {
2932                    Ok(media_query_list) => swc_css_ast::Stylesheet {
2933                        span: Default::default(),
2934                        rules: vec![swc_css_ast::Rule::AtRule(
2935                            swc_css_ast::AtRule {
2936                                span: Default::default(),
2937                                name: swc_css_ast::AtRuleName::Ident(swc_css_ast::Ident {
2938                                    span: Default::default(),
2939                                    value: atom!("media"),
2940                                    raw: None,
2941                                }),
2942                                prelude: Some(
2943                                    swc_css_ast::AtRulePrelude::MediaPrelude(media_query_list)
2944                                        .into(),
2945                                ),
2946                                block: Some(swc_css_ast::SimpleBlock {
2947                                    span: Default::default(),
2948                                    name: swc_css_ast::TokenAndSpan {
2949                                        span: DUMMY_SP,
2950                                        token: swc_css_ast::Token::LBrace,
2951                                    },
2952                                    // TODO make the `compress_empty` option for CSS minifier and
2953                                    // remove it
2954                                    value: vec![swc_css_ast::ComponentValue::Str(Box::new(
2955                                        swc_css_ast::Str {
2956                                            span: Default::default(),
2957                                            value: atom!("placeholder"),
2958                                            raw: None,
2959                                        },
2960                                    ))],
2961                                }),
2962                            }
2963                            .into(),
2964                        )],
2965                    },
2966                    _ => return None,
2967                }
2968            }
2969        };
2970
2971        // Avoid compress potential invalid CSS
2972        if !errors.is_empty() {
2973            return None;
2974        }
2975
2976        swc_css_minifier::minify(&mut stylesheet, options.minifier);
2977
2978        let mut minified = String::new();
2979        let wr = swc_css_codegen::writer::basic::BasicCssWriter::new(
2980            &mut minified,
2981            None,
2982            swc_css_codegen::writer::basic::BasicCssWriterConfig::default(),
2983        );
2984
2985        options.codegen.minify = true;
2986
2987        let mut gen = swc_css_codegen::CodeGenerator::new(wr, options.codegen);
2988
2989        match mode {
2990            CssMinificationMode::Stylesheet => {
2991                swc_css_codegen::Emit::emit(&mut gen, &stylesheet).unwrap();
2992            }
2993            CssMinificationMode::ListOfDeclarations => {
2994                let swc_css_ast::Stylesheet { rules, .. } = &stylesheet;
2995
2996                // Because CSS is grammar free, protect for fails
2997                let Some(swc_css_ast::Rule::QualifiedRule(qualified_rule)) = rules.first() else {
2998                    return None;
2999                };
3000
3001                let swc_css_ast::QualifiedRule { block, .. } = &**qualified_rule;
3002
3003                swc_css_codegen::Emit::emit(&mut gen, &block).unwrap();
3004
3005                minified = minified[1..minified.len() - 1].to_string();
3006            }
3007            CssMinificationMode::MediaQueryList => {
3008                let swc_css_ast::Stylesheet { rules, .. } = &stylesheet;
3009
3010                // Because CSS is grammar free, protect for fails
3011                let Some(swc_css_ast::Rule::AtRule(at_rule)) = rules.first() else {
3012                    return None;
3013                };
3014
3015                let swc_css_ast::AtRule { prelude, .. } = &**at_rule;
3016
3017                swc_css_codegen::Emit::emit(&mut gen, &prelude).unwrap();
3018
3019                minified = minified.trim().to_string();
3020            }
3021        }
3022
3023        Some(minified)
3024    }
3025}
3026
3027fn create_minifier<'a, C: MinifyCss>(
3028    context_element: Option<&Element>,
3029    options: &'a MinifyOptions<C::Options>,
3030    css_minifier: &'a C,
3031) -> Minifier<'a, C> {
3032    let mut current_element = None;
3033    let mut is_pre = false;
3034
3035    if let Some(context_element) = context_element {
3036        current_element = Some(context_element.clone());
3037        is_pre = get_white_space(context_element.namespace, &context_element.tag_name)
3038            == WhiteSpace::Pre;
3039    }
3040
3041    Minifier {
3042        options,
3043
3044        current_element,
3045        latest_element: None,
3046        descendant_of_pre: is_pre,
3047        attribute_name_counter: None,
3048
3049        css_minifier,
3050    }
3051}
3052
3053pub fn minify_document_with_custom_css_minifier<C: MinifyCss>(
3054    document: &mut Document,
3055    options: &MinifyOptions<C::Options>,
3056    css_minifier: &C,
3057) {
3058    let mut minifier = create_minifier(None, options, css_minifier);
3059
3060    if options.sort_attributes {
3061        let mut attribute_name_counter = AttributeNameCounter {
3062            tree: Default::default(),
3063        };
3064
3065        document.visit_mut_with(&mut attribute_name_counter);
3066
3067        minifier.attribute_name_counter = Some(attribute_name_counter.tree);
3068    }
3069
3070    document.visit_mut_with(&mut minifier);
3071}
3072
3073pub fn minify_document_fragment_with_custom_css_minifier<C: MinifyCss>(
3074    document_fragment: &mut DocumentFragment,
3075    context_element: &Element,
3076    options: &MinifyOptions<C::Options>,
3077    css_minifier: &C,
3078) {
3079    let mut minifier = create_minifier(Some(context_element), options, css_minifier);
3080
3081    if options.sort_attributes {
3082        let mut attribute_name_counter = AttributeNameCounter {
3083            tree: Default::default(),
3084        };
3085
3086        document_fragment.visit_mut_with(&mut attribute_name_counter);
3087
3088        minifier.attribute_name_counter = Some(attribute_name_counter.tree);
3089    }
3090
3091    document_fragment.visit_mut_with(&mut minifier);
3092}
3093
3094#[cfg(feature = "default-css-minifier")]
3095pub fn minify_document(document: &mut Document, options: &MinifyOptions<CssOptions>) {
3096    minify_document_with_custom_css_minifier(document, options, &DefaultCssMinifier)
3097}
3098
3099#[cfg(feature = "default-css-minifier")]
3100pub fn minify_document_fragment(
3101    document_fragment: &mut DocumentFragment,
3102    context_element: &Element,
3103    options: &MinifyOptions<CssOptions>,
3104) {
3105    minify_document_fragment_with_custom_css_minifier(
3106        document_fragment,
3107        context_element,
3108        options,
3109        &DefaultCssMinifier,
3110    )
3111}