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        }
1928
1929        if is_modules {
1930            swc_ecma_visit::VisitMutWith::visit_mut_with(
1931                &mut left_program,
1932                &mut swc_ecma_transforms_base::hygiene::hygiene(),
1933            );
1934        }
1935
1936        let left_program =
1937            left_program.apply(swc_ecma_transforms_base::fixer::fixer(Some(&comments)));
1938
1939        let mut buf = Vec::new();
1940
1941        {
1942            let wr = Box::new(swc_ecma_codegen::text_writer::JsWriter::new(
1943                cm.clone(),
1944                "\n",
1945                &mut buf,
1946                None,
1947            )) as Box<dyn swc_ecma_codegen::text_writer::WriteJs>;
1948
1949            let mut emitter = swc_ecma_codegen::Emitter {
1950                cfg: swc_ecma_codegen::Config::default().with_target(target),
1951                cm,
1952                comments: Some(&comments),
1953                wr,
1954            };
1955
1956            emitter.emit_program(&left_program).unwrap();
1957        }
1958
1959        let code = match String::from_utf8(buf) {
1960            Ok(minified) => minified,
1961            _ => return None,
1962        };
1963
1964        Some(code)
1965    }
1966
1967    // TODO source map url output for JS and CSS?
1968    fn minify_js(&self, data: String, is_module: bool, is_attribute: bool) -> Option<String> {
1969        let mut errors: Vec<_> = Vec::new();
1970
1971        let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
1972        let fm = cm.new_source_file(FileName::Anon.into(), data);
1973        let mut options = self.get_js_options();
1974
1975        if let swc_ecma_parser::Syntax::Es(es_config) = &mut options.parser.syntax {
1976            es_config.allow_return_outside_function = !is_module && is_attribute;
1977        }
1978
1979        let comments = SingleThreadedComments::default();
1980
1981        let mut program = if is_module {
1982            match swc_ecma_parser::parse_file_as_module(
1983                &fm,
1984                options.parser.syntax,
1985                options.parser.target,
1986                if options.parser.comments {
1987                    Some(&comments)
1988                } else {
1989                    None
1990                },
1991                &mut errors,
1992            ) {
1993                Ok(module) => swc_ecma_ast::Program::Module(module),
1994                _ => return None,
1995            }
1996        } else {
1997            match swc_ecma_parser::parse_file_as_script(
1998                &fm,
1999                options.parser.syntax,
2000                options.parser.target,
2001                if options.parser.comments {
2002                    Some(&comments)
2003                } else {
2004                    None
2005                },
2006                &mut errors,
2007            ) {
2008                Ok(script) => swc_ecma_ast::Program::Script(script),
2009                _ => return None,
2010            }
2011        };
2012
2013        // Avoid compress potential invalid JS
2014        if !errors.is_empty() {
2015            return None;
2016        }
2017
2018        if let Some(compress_options) = &mut options.minifier.compress {
2019            compress_options.module = is_module;
2020        } else {
2021            options.minifier.compress = Some(swc_ecma_minifier::option::CompressOptions {
2022                ecma: options.parser.target,
2023                ..Default::default()
2024            });
2025        }
2026
2027        let unresolved_mark = Mark::new();
2028        let top_level_mark = Mark::new();
2029
2030        swc_ecma_visit::VisitMutWith::visit_mut_with(
2031            &mut program,
2032            &mut swc_ecma_transforms_base::resolver(unresolved_mark, top_level_mark, false),
2033        );
2034
2035        let program = program.apply(swc_ecma_transforms_base::fixer::paren_remover(Some(
2036            &comments,
2037        )));
2038
2039        let program = swc_ecma_minifier::optimize(
2040            program,
2041            cm.clone(),
2042            if options.parser.comments {
2043                Some(&comments)
2044            } else {
2045                None
2046            },
2047            None,
2048            // TODO allow to keep `var`/function/etc on top level
2049            &options.minifier,
2050            &swc_ecma_minifier::option::ExtraOptions {
2051                unresolved_mark,
2052                top_level_mark,
2053                mangle_name_cache: None,
2054            },
2055        );
2056
2057        let program = program.apply(swc_ecma_transforms_base::fixer::fixer(Some(&comments)));
2058
2059        let mut buf = Vec::new();
2060
2061        {
2062            let mut wr = Box::new(swc_ecma_codegen::text_writer::JsWriter::new(
2063                cm.clone(),
2064                "\n",
2065                &mut buf,
2066                None,
2067            )) as Box<dyn swc_ecma_codegen::text_writer::WriteJs>;
2068
2069            wr = Box::new(swc_ecma_codegen::text_writer::omit_trailing_semi(wr));
2070
2071            options.codegen.minify = true;
2072            options.codegen.target = options.parser.target;
2073            options.codegen.omit_last_semi = true;
2074
2075            let mut emitter = swc_ecma_codegen::Emitter {
2076                cfg: options.codegen,
2077                cm,
2078                comments: if options.parser.comments {
2079                    Some(&comments)
2080                } else {
2081                    None
2082                },
2083                wr,
2084            };
2085
2086            emitter.emit_program(&program).unwrap();
2087        }
2088
2089        let minified = match String::from_utf8(buf) {
2090            // Avoid generating the sequence "</script" in JS code
2091            // TODO move it to ecma codegen under the option?
2092            Ok(minified) => minified.replace("</script>", "<\\/script>"),
2093            _ => return None,
2094        };
2095
2096        Some(minified)
2097    }
2098
2099    fn need_minify_css(&self) -> bool {
2100        match self.options.minify_css {
2101            MinifyCssOption::Bool(value) => value,
2102            MinifyCssOption::Options(_) => true,
2103        }
2104    }
2105
2106    fn minify_sizes(&self, value: &str) -> Option<String> {
2107        let values = value
2108            .rsplitn(2, ['\t', '\n', '\x0C', '\r', ' '])
2109            .collect::<Vec<&str>>();
2110
2111        if values.len() != 2 {
2112            return None;
2113        }
2114
2115        if !values[1].starts_with('(') {
2116            return None;
2117        }
2118
2119        let media_condition =
2120            // It should be `MediaCondition`, but `<media-query> = <media-condition>` and other values is just invalid size
2121            match self.minify_css(values[1].to_string(), CssMinificationMode::MediaQueryList) {
2122                Some(minified) => minified,
2123                _ => return None,
2124            };
2125
2126        let source_size_value = values[0];
2127        let mut minified = String::with_capacity(media_condition.len() + source_size_value.len());
2128
2129        minified.push_str(&media_condition);
2130        minified.push(' ');
2131        minified.push_str(source_size_value);
2132
2133        Some(minified)
2134    }
2135
2136    fn minify_css(&self, data: String, mode: CssMinificationMode) -> Option<String> {
2137        self.css_minifier
2138            .minify_css(&self.options.minify_css, data, mode)
2139    }
2140
2141    fn minify_html(&self, data: String, mode: HtmlMinificationMode) -> Option<String> {
2142        let mut errors: Vec<_> = Vec::new();
2143
2144        let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
2145        let fm = cm.new_source_file(FileName::Anon.into(), data);
2146
2147        // Emulate content inside conditional comments like content inside the
2148        // `template` element
2149        let mut context_element = None;
2150
2151        let mut document_or_document_fragment = match mode {
2152            HtmlMinificationMode::ConditionalComments => {
2153                // Emulate content inside conditional comments like content inside the
2154                // `template` element, because it can be used in any place in source code
2155                context_element = Some(Element {
2156                    span: Default::default(),
2157                    tag_name: atom!("template"),
2158                    namespace: Namespace::HTML,
2159                    attributes: Vec::new(),
2160                    children: Vec::new(),
2161                    content: None,
2162                    is_self_closing: false,
2163                });
2164
2165                match swc_html_parser::parse_file_as_document_fragment(
2166                    &fm,
2167                    context_element.as_ref().unwrap(),
2168                    DocumentMode::NoQuirks,
2169                    None,
2170                    Default::default(),
2171                    &mut errors,
2172                ) {
2173                    Ok(document_fragment) => HtmlRoot::DocumentFragment(document_fragment),
2174                    _ => return None,
2175                }
2176            }
2177            HtmlMinificationMode::DocumentIframeSrcdoc => {
2178                match swc_html_parser::parse_file_as_document(
2179                    &fm,
2180                    ParserConfig {
2181                        iframe_srcdoc: true,
2182                        ..Default::default()
2183                    },
2184                    &mut errors,
2185                ) {
2186                    Ok(document) => HtmlRoot::Document(document),
2187                    _ => return None,
2188                }
2189            }
2190        };
2191
2192        // Avoid compress potential invalid CSS
2193        if !errors.is_empty() {
2194            return None;
2195        }
2196
2197        match document_or_document_fragment {
2198            HtmlRoot::Document(ref mut document) => {
2199                minify_document_with_custom_css_minifier(document, self.options, self.css_minifier);
2200            }
2201            HtmlRoot::DocumentFragment(ref mut document_fragment) => {
2202                minify_document_fragment_with_custom_css_minifier(
2203                    document_fragment,
2204                    context_element.as_ref().unwrap(),
2205                    self.options,
2206                    self.css_minifier,
2207                )
2208            }
2209        }
2210
2211        let mut minified = String::new();
2212        let wr = swc_html_codegen::writer::basic::BasicHtmlWriter::new(
2213            &mut minified,
2214            None,
2215            swc_html_codegen::writer::basic::BasicHtmlWriterConfig::default(),
2216        );
2217        let mut gen = swc_html_codegen::CodeGenerator::new(
2218            wr,
2219            swc_html_codegen::CodegenConfig {
2220                minify: true,
2221                scripting_enabled: false,
2222                context_element: context_element.as_ref(),
2223                tag_omission: None,
2224                keep_head_and_body: None,
2225                self_closing_void_elements: None,
2226                quotes: None,
2227            },
2228        );
2229
2230        match document_or_document_fragment {
2231            HtmlRoot::Document(document) => {
2232                swc_html_codegen::Emit::emit(&mut gen, &document).unwrap();
2233            }
2234            HtmlRoot::DocumentFragment(document_fragment) => {
2235                swc_html_codegen::Emit::emit(&mut gen, &document_fragment).unwrap();
2236            }
2237        }
2238
2239        Some(minified)
2240    }
2241
2242    fn minify_attribute(&self, element: &Element, n: &mut Attribute) {
2243        if let Some(value) = &n.value {
2244            if value.is_empty() {
2245                if (self.options.collapse_boolean_attributes
2246                    && self.is_boolean_attribute(element, n))
2247                    || (self.options.normalize_attributes
2248                        && self.is_crossorigin_attribute(element, n)
2249                        && value.is_empty())
2250                {
2251                    n.value = None;
2252                }
2253
2254                return;
2255            }
2256
2257            match (element.namespace, &*element.tag_name, &*n.name) {
2258                (Namespace::HTML, "iframe", "srcdoc") => {
2259                    if let Some(minified) = self.minify_html(
2260                        value.to_string(),
2261                        HtmlMinificationMode::DocumentIframeSrcdoc,
2262                    ) {
2263                        n.value = Some(minified.into());
2264                    };
2265                }
2266                (
2267                    Namespace::HTML | Namespace::SVG,
2268                    "style" | "link" | "script" | "input",
2269                    "type",
2270                ) if self.options.normalize_attributes => {
2271                    n.value = Some(value.trim().to_ascii_lowercase().into());
2272                }
2273                _ if self.options.normalize_attributes
2274                    && self.is_crossorigin_attribute(element, n)
2275                    && value.to_ascii_lowercase() == "anonymous" =>
2276                {
2277                    n.value = None;
2278                }
2279                _ if self.options.collapse_boolean_attributes
2280                    && self.is_boolean_attribute(element, n) =>
2281                {
2282                    n.value = None;
2283                }
2284                _ if self.is_event_handler_attribute(n) => {
2285                    let mut value = value.to_string();
2286
2287                    if self.options.normalize_attributes {
2288                        value = value.trim().into();
2289
2290                        if value.trim().to_lowercase().starts_with("javascript:") {
2291                            value = value.chars().skip(11).collect();
2292                        }
2293                    }
2294
2295                    if self.need_minify_js() {
2296                        if let Some(minified) = self.minify_js(value, false, true) {
2297                            n.value = Some(minified.into());
2298                        };
2299                    } else {
2300                        n.value = Some(value.into());
2301                    }
2302                }
2303                _ if self.options.normalize_attributes
2304                    && element.namespace == Namespace::HTML
2305                    && n.name == "contenteditable"
2306                    && n.value.as_deref() == Some("true") =>
2307                {
2308                    n.value = Some(atom!(""));
2309                }
2310                _ if self.options.normalize_attributes
2311                    && self.is_semicolon_separated_attribute(element, n) =>
2312                {
2313                    n.value = Some(
2314                        value
2315                            .split(';')
2316                            .map(|value| self.collapse_whitespace(value.trim()))
2317                            .collect::<Vec<_>>()
2318                            .join(";")
2319                            .into(),
2320                    );
2321                }
2322                _ if self.options.normalize_attributes
2323                    && n.name == "content"
2324                    && self.element_has_attribute_with_value(
2325                        element,
2326                        "http-equiv",
2327                        &["content-security-policy"],
2328                    ) =>
2329                {
2330                    let mut new_values = Vec::new();
2331
2332                    for value in value.trim().split(';') {
2333                        new_values.push(
2334                            value
2335                                .trim()
2336                                .split(' ')
2337                                .filter(|s| !s.is_empty())
2338                                .collect::<Vec<_>>()
2339                                .join(" "),
2340                        );
2341                    }
2342
2343                    let mut value = new_values.join(";");
2344
2345                    if value.ends_with(';') {
2346                        value.pop();
2347                    }
2348
2349                    n.value = Some(value.into());
2350                }
2351                _ if self.options.sort_space_separated_attribute_values
2352                    && self.is_attribute_value_unordered_set(element, n) =>
2353                {
2354                    let mut values = value.split_whitespace().collect::<Vec<_>>();
2355
2356                    values.sort_unstable();
2357
2358                    n.value = Some(values.join(" ").into());
2359                }
2360                _ if self.options.normalize_attributes
2361                    && self.is_space_separated_attribute(element, n) =>
2362                {
2363                    n.value = Some(
2364                        value
2365                            .split_whitespace()
2366                            .collect::<Vec<_>>()
2367                            .join(" ")
2368                            .into(),
2369                    );
2370                }
2371                _ if self.is_comma_separated_attribute(element, n) => {
2372                    let mut value = value.to_string();
2373
2374                    if self.options.normalize_attributes {
2375                        value = value
2376                            .split(',')
2377                            .map(|value| {
2378                                if matches!(&*n.name, "sizes" | "imagesizes") {
2379                                    let trimmed = value.trim();
2380
2381                                    match self.minify_sizes(trimmed) {
2382                                        Some(minified) => minified,
2383                                        _ => trimmed.to_string(),
2384                                    }
2385                                } else if matches!(&*n.name, "points") {
2386                                    self.collapse_whitespace(value.trim()).to_string()
2387                                } else if matches!(&*n.name, "exportparts") {
2388                                    value.chars().filter(|c| !c.is_whitespace()).collect()
2389                                } else {
2390                                    value.trim().to_string()
2391                                }
2392                            })
2393                            .collect::<Vec<_>>()
2394                            .join(",");
2395                    }
2396
2397                    if self.need_minify_css() && n.name == "media" && !value.is_empty() {
2398                        if let Some(minified) =
2399                            self.minify_css(value, CssMinificationMode::MediaQueryList)
2400                        {
2401                            n.value = Some(minified.into());
2402                        }
2403                    } else {
2404                        n.value = Some(value.into());
2405                    }
2406                }
2407                _ if self.is_trimable_separated_attribute(element, n) => {
2408                    let mut value = value.to_string();
2409
2410                    let fallback = |n: &mut Attribute| {
2411                        if self.options.normalize_attributes {
2412                            n.value = Some(value.trim().into());
2413                        }
2414                    };
2415
2416                    if self.need_minify_css() && n.name == "style" && !value.is_empty() {
2417                        let value = value.trim();
2418
2419                        if let Some(minified) = self
2420                            .minify_css(value.to_string(), CssMinificationMode::ListOfDeclarations)
2421                        {
2422                            n.value = Some(minified.into());
2423                        } else {
2424                            fallback(n);
2425                        }
2426                    } else if self.need_minify_js() && self.is_javascript_url_element(element) {
2427                        if value.trim().to_lowercase().starts_with("javascript:") {
2428                            value = value.trim().chars().skip(11).collect();
2429
2430                            if let Some(minified) = self.minify_js(value, false, true) {
2431                                let mut with_javascript =
2432                                    String::with_capacity(11 + minified.len());
2433
2434                                with_javascript.push_str("javascript:");
2435                                with_javascript.push_str(&minified);
2436
2437                                n.value = Some(with_javascript.into());
2438                            }
2439                        } else {
2440                            fallback(n);
2441                        }
2442                    } else {
2443                        fallback(n);
2444                    }
2445                }
2446                _ if self.options.minify_additional_attributes.is_some() => {
2447                    match self.is_additional_minifier_attribute(&n.name) {
2448                        Some(MinifierType::JsScript) if self.need_minify_js() => {
2449                            if let Some(minified) = self.minify_js(value.to_string(), false, true) {
2450                                n.value = Some(minified.into());
2451                            }
2452                        }
2453                        Some(MinifierType::JsModule) if self.need_minify_js() => {
2454                            if let Some(minified) = self.minify_js(value.to_string(), true, true) {
2455                                n.value = Some(minified.into());
2456                            }
2457                        }
2458                        Some(MinifierType::Json) if self.need_minify_json() => {
2459                            if let Some(minified) = self.minify_json(value.to_string()) {
2460                                n.value = Some(minified.into());
2461                            }
2462                        }
2463                        Some(MinifierType::Css) if self.need_minify_css() => {
2464                            if let Some(minified) = self.minify_css(
2465                                value.to_string(),
2466                                CssMinificationMode::ListOfDeclarations,
2467                            ) {
2468                                n.value = Some(minified.into());
2469                            }
2470                        }
2471                        Some(MinifierType::Html) => {
2472                            if let Some(minified) = self.minify_html(
2473                                value.to_string(),
2474                                HtmlMinificationMode::DocumentIframeSrcdoc,
2475                            ) {
2476                                n.value = Some(minified.into());
2477                            };
2478                        }
2479                        _ => {}
2480                    }
2481                }
2482                _ => {}
2483            }
2484        }
2485    }
2486}
2487
2488impl<C: MinifyCss> VisitMut for Minifier<'_, C> {
2489    fn visit_mut_document(&mut self, n: &mut Document) {
2490        n.visit_mut_children_with(self);
2491
2492        n.children
2493            .retain(|child| !matches!(child, Child::Comment(_) if self.options.remove_comments));
2494    }
2495
2496    fn visit_mut_document_fragment(&mut self, n: &mut DocumentFragment) {
2497        n.children = self.minify_children(&mut n.children);
2498
2499        n.visit_mut_children_with(self);
2500    }
2501
2502    fn visit_mut_document_type(&mut self, n: &mut DocumentType) {
2503        n.visit_mut_children_with(self);
2504
2505        if !self.options.force_set_html5_doctype {
2506            return;
2507        }
2508
2509        n.name = Some(atom!("html"));
2510        n.system_id = None;
2511        n.public_id = None;
2512    }
2513
2514    fn visit_mut_child(&mut self, n: &mut Child) {
2515        n.visit_mut_children_with(self);
2516
2517        self.current_element = None;
2518
2519        if matches!(
2520            self.options.collapse_whitespaces,
2521            CollapseWhitespaces::Smart
2522                | CollapseWhitespaces::AdvancedConservative
2523                | CollapseWhitespaces::OnlyMetadata
2524        ) {
2525            match n {
2526                Child::Text(_) | Child::Element(_) => {
2527                    self.latest_element = Some(n.clone());
2528                }
2529                _ => {}
2530            }
2531        }
2532    }
2533
2534    fn visit_mut_element(&mut self, n: &mut Element) {
2535        // Don't copy children to save memory
2536        self.current_element = Some(Element {
2537            span: Default::default(),
2538            tag_name: n.tag_name.clone(),
2539            namespace: n.namespace,
2540            attributes: n.attributes.clone(),
2541            children: Vec::new(),
2542            content: None,
2543            is_self_closing: n.is_self_closing,
2544        });
2545
2546        let old_descendant_of_pre = self.descendant_of_pre;
2547
2548        if self.need_collapse_whitespace() && !old_descendant_of_pre {
2549            self.descendant_of_pre = get_white_space(n.namespace, &n.tag_name) == WhiteSpace::Pre;
2550        }
2551
2552        n.children = self.minify_children(&mut n.children);
2553
2554        n.visit_mut_children_with(self);
2555
2556        // Remove all leading and trailing whitespaces for the `body` element
2557        if n.namespace == Namespace::HTML && n.tag_name == "body" && self.need_collapse_whitespace()
2558        {
2559            self.remove_leading_and_trailing_whitespaces(&mut n.children, true, true);
2560        }
2561
2562        if self.need_collapse_whitespace() {
2563            self.descendant_of_pre = old_descendant_of_pre;
2564        }
2565
2566        let mut remove_list = Vec::new();
2567
2568        for (i, i1) in n.attributes.iter().enumerate() {
2569            if i1.value.is_some() {
2570                if self.options.remove_redundant_attributes != RemoveRedundantAttributes::None
2571                    && self.is_default_attribute_value(n, i1)
2572                {
2573                    remove_list.push(i);
2574
2575                    continue;
2576                }
2577
2578                if self.options.remove_empty_attributes {
2579                    let value = i1.value.as_ref().unwrap();
2580
2581                    if (matches!(&*i1.name, "id") && value.is_empty())
2582                        || (matches!(&*i1.name, "class" | "style") && value.is_empty())
2583                        || self.is_event_handler_attribute(i1) && value.is_empty()
2584                    {
2585                        remove_list.push(i);
2586
2587                        continue;
2588                    }
2589                }
2590            }
2591
2592            for (j, j1) in n.attributes.iter().enumerate() {
2593                if i < j && i1.name == j1.name {
2594                    remove_list.push(j);
2595                }
2596            }
2597        }
2598
2599        // Fast path. We don't face real duplicates in most cases.
2600        if !remove_list.is_empty() {
2601            let new = take(&mut n.attributes)
2602                .into_iter()
2603                .enumerate()
2604                .filter_map(|(idx, value)| {
2605                    if remove_list.contains(&idx) {
2606                        None
2607                    } else {
2608                        Some(value)
2609                    }
2610                })
2611                .collect::<Vec<_>>();
2612
2613            n.attributes = new;
2614        }
2615
2616        if let Some(attribute_name_counter) = &self.attribute_name_counter {
2617            n.attributes.sort_by(|a, b| {
2618                let ordeing = attribute_name_counter
2619                    .get(&b.name)
2620                    .cmp(&attribute_name_counter.get(&a.name));
2621
2622                match ordeing {
2623                    Ordering::Equal => b.name.cmp(&a.name),
2624                    _ => ordeing,
2625                }
2626            });
2627        }
2628    }
2629
2630    fn visit_mut_attribute(&mut self, n: &mut Attribute) {
2631        n.visit_mut_children_with(self);
2632
2633        let element = match &self.current_element {
2634            Some(current_element) => current_element,
2635            _ => return,
2636        };
2637
2638        self.minify_attribute(element, n);
2639    }
2640
2641    fn visit_mut_text(&mut self, n: &mut Text) {
2642        n.visit_mut_children_with(self);
2643
2644        if n.data.is_empty() {
2645            return;
2646        }
2647
2648        let mut text_type = None;
2649
2650        if let Some(current_element) = &self.current_element {
2651            match &*current_element.tag_name {
2652                "script"
2653                    if (self.need_minify_json() || self.need_minify_js())
2654                        && matches!(
2655                            current_element.namespace,
2656                            Namespace::HTML | Namespace::SVG
2657                        )
2658                        && !current_element
2659                            .attributes
2660                            .iter()
2661                            .any(|attribute| matches!(&*attribute.name, "src")) =>
2662                {
2663                    let type_attribute_value: Option<Atom> = self
2664                        .get_attribute_value(&current_element.attributes, "type")
2665                        .map(|v| v.to_ascii_lowercase().trim().into());
2666
2667                    match type_attribute_value.as_deref() {
2668                        Some("module") if self.need_minify_js() => {
2669                            text_type = Some(MinifierType::JsModule);
2670                        }
2671                        Some(value)
2672                            if self.need_minify_js() && self.is_type_text_javascript(value) =>
2673                        {
2674                            text_type = Some(MinifierType::JsScript);
2675                        }
2676                        None if self.need_minify_js() => {
2677                            text_type = Some(MinifierType::JsScript);
2678                        }
2679                        Some(
2680                            "application/json"
2681                            | "application/ld+json"
2682                            | "importmap"
2683                            | "speculationrules",
2684                        ) if self.need_minify_json() => {
2685                            text_type = Some(MinifierType::Json);
2686                        }
2687                        Some(script_type)
2688                            if self.options.minify_additional_scripts_content.is_some() =>
2689                        {
2690                            if let Some(minifier_type) =
2691                                self.is_additional_scripts_content(script_type)
2692                            {
2693                                text_type = Some(minifier_type);
2694                            }
2695                        }
2696                        _ => {}
2697                    }
2698                }
2699                "style"
2700                    if self.need_minify_css()
2701                        && matches!(
2702                            current_element.namespace,
2703                            Namespace::HTML | Namespace::SVG
2704                        ) =>
2705                {
2706                    let mut type_attribute_value = None;
2707
2708                    for attribute in &current_element.attributes {
2709                        if attribute.name == "type" && attribute.value.is_some() {
2710                            type_attribute_value = Some(attribute.value.as_ref().unwrap());
2711
2712                            break;
2713                        }
2714                    }
2715
2716                    if type_attribute_value.is_none()
2717                        || self.is_type_text_css(type_attribute_value.as_ref().unwrap())
2718                    {
2719                        text_type = Some(MinifierType::Css)
2720                    }
2721                }
2722                "title" if current_element.namespace == Namespace::HTML => {
2723                    n.data = self.collapse_whitespace(&n.data).trim().into();
2724                }
2725                _ => {}
2726            }
2727        }
2728
2729        match text_type {
2730            Some(MinifierType::JsScript) => {
2731                let minified = match self.minify_js(n.data.to_string(), false, false) {
2732                    Some(minified) => minified,
2733                    None => return,
2734                };
2735
2736                n.data = minified.into();
2737            }
2738            Some(MinifierType::JsModule) => {
2739                let minified = match self.minify_js(n.data.to_string(), true, false) {
2740                    Some(minified) => minified,
2741                    None => return,
2742                };
2743
2744                n.data = minified.into();
2745            }
2746            Some(MinifierType::Json) => {
2747                let minified = match self.minify_json(n.data.to_string()) {
2748                    Some(minified) => minified,
2749                    None => return,
2750                };
2751
2752                n.data = minified.into();
2753            }
2754            Some(MinifierType::Css) => {
2755                let minified =
2756                    match self.minify_css(n.data.to_string(), CssMinificationMode::Stylesheet) {
2757                        Some(minified) => minified,
2758                        None => return,
2759                    };
2760
2761                n.data = minified.into();
2762            }
2763            Some(MinifierType::Html) => {
2764                let minified = match self.minify_html(
2765                    n.data.to_string(),
2766                    HtmlMinificationMode::ConditionalComments,
2767                ) {
2768                    Some(minified) => minified,
2769                    None => return,
2770                };
2771
2772                n.data = minified.into();
2773            }
2774            _ => {}
2775        }
2776    }
2777
2778    fn visit_mut_comment(&mut self, n: &mut Comment) {
2779        n.visit_mut_children_with(self);
2780
2781        if !self.options.minify_conditional_comments {
2782            return;
2783        }
2784
2785        if self.is_conditional_comment(&n.data) && !n.data.is_empty() {
2786            let start_pos = match n.data.find("]>") {
2787                Some(start_pos) => start_pos,
2788                _ => return,
2789            };
2790            let end_pos = match n.data.find("<![") {
2791                Some(end_pos) => end_pos,
2792                _ => return,
2793            };
2794
2795            let html = n
2796                .data
2797                .chars()
2798                .skip(start_pos)
2799                .take(end_pos - start_pos)
2800                .collect();
2801
2802            let minified = match self.minify_html(html, HtmlMinificationMode::ConditionalComments) {
2803                Some(minified) => minified,
2804                _ => return,
2805            };
2806            let before: String = n.data.chars().take(start_pos).collect();
2807            let after: String = n.data.chars().skip(end_pos).take(n.data.len()).collect();
2808            let mut data = String::with_capacity(n.data.len());
2809
2810            data.push_str(&before);
2811            data.push_str(&minified);
2812            data.push_str(&after);
2813
2814            n.data = data.into();
2815        }
2816    }
2817}
2818
2819struct AttributeNameCounter {
2820    tree: FxHashMap<Atom, usize>,
2821}
2822
2823impl VisitMut for AttributeNameCounter {
2824    fn visit_mut_attribute(&mut self, n: &mut Attribute) {
2825        n.visit_mut_children_with(self);
2826
2827        *self.tree.entry(n.name.clone()).or_insert(0) += 1;
2828    }
2829}
2830
2831pub trait MinifyCss {
2832    type Options;
2833    fn minify_css(
2834        &self,
2835        options: &MinifyCssOption<Self::Options>,
2836        data: String,
2837        mode: CssMinificationMode,
2838    ) -> Option<String>;
2839}
2840
2841#[cfg(feature = "default-css-minifier")]
2842struct DefaultCssMinifier;
2843
2844#[cfg(feature = "default-css-minifier")]
2845impl DefaultCssMinifier {
2846    fn get_css_options(&self, options: &MinifyCssOption<CssOptions>) -> CssOptions {
2847        match options {
2848            MinifyCssOption::Bool(_) => CssOptions {
2849                parser: swc_css_parser::parser::ParserConfig::default(),
2850                minifier: swc_css_minifier::options::MinifyOptions::default(),
2851                codegen: swc_css_codegen::CodegenConfig::default(),
2852            },
2853            MinifyCssOption::Options(css_options) => css_options.clone(),
2854        }
2855    }
2856}
2857
2858#[cfg(feature = "default-css-minifier")]
2859impl MinifyCss for DefaultCssMinifier {
2860    type Options = CssOptions;
2861
2862    fn minify_css(
2863        &self,
2864        options: &MinifyCssOption<Self::Options>,
2865        data: String,
2866        mode: CssMinificationMode,
2867    ) -> Option<String> {
2868        let mut errors: Vec<_> = Vec::new();
2869
2870        let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
2871        let fm = cm.new_source_file(FileName::Anon.into(), data);
2872
2873        let mut options = self.get_css_options(options);
2874
2875        let mut stylesheet = match mode {
2876            CssMinificationMode::Stylesheet => {
2877                match swc_css_parser::parse_file(&fm, None, options.parser, &mut errors) {
2878                    Ok(stylesheet) => stylesheet,
2879                    _ => return None,
2880                }
2881            }
2882            CssMinificationMode::ListOfDeclarations => {
2883                match swc_css_parser::parse_file::<Vec<swc_css_ast::DeclarationOrAtRule>>(
2884                    &fm,
2885                    None,
2886                    options.parser,
2887                    &mut errors,
2888                ) {
2889                    Ok(list_of_declarations) => {
2890                        let declaration_list: Vec<swc_css_ast::ComponentValue> =
2891                            list_of_declarations
2892                                .into_iter()
2893                                .map(|node| node.into())
2894                                .collect();
2895
2896                        swc_css_ast::Stylesheet {
2897                            span: Default::default(),
2898                            rules: vec![swc_css_ast::Rule::QualifiedRule(
2899                                swc_css_ast::QualifiedRule {
2900                                    span: Default::default(),
2901                                    prelude: swc_css_ast::QualifiedRulePrelude::SelectorList(
2902                                        swc_css_ast::SelectorList {
2903                                            span: Default::default(),
2904                                            children: Vec::new(),
2905                                        },
2906                                    ),
2907                                    block: swc_css_ast::SimpleBlock {
2908                                        span: Default::default(),
2909                                        name: swc_css_ast::TokenAndSpan {
2910                                            span: DUMMY_SP,
2911                                            token: swc_css_ast::Token::LBrace,
2912                                        },
2913                                        value: declaration_list,
2914                                    },
2915                                }
2916                                .into(),
2917                            )],
2918                        }
2919                    }
2920                    _ => return None,
2921                }
2922            }
2923            CssMinificationMode::MediaQueryList => {
2924                match swc_css_parser::parse_file::<swc_css_ast::MediaQueryList>(
2925                    &fm,
2926                    None,
2927                    options.parser,
2928                    &mut errors,
2929                ) {
2930                    Ok(media_query_list) => swc_css_ast::Stylesheet {
2931                        span: Default::default(),
2932                        rules: vec![swc_css_ast::Rule::AtRule(
2933                            swc_css_ast::AtRule {
2934                                span: Default::default(),
2935                                name: swc_css_ast::AtRuleName::Ident(swc_css_ast::Ident {
2936                                    span: Default::default(),
2937                                    value: atom!("media"),
2938                                    raw: None,
2939                                }),
2940                                prelude: Some(
2941                                    swc_css_ast::AtRulePrelude::MediaPrelude(media_query_list)
2942                                        .into(),
2943                                ),
2944                                block: Some(swc_css_ast::SimpleBlock {
2945                                    span: Default::default(),
2946                                    name: swc_css_ast::TokenAndSpan {
2947                                        span: DUMMY_SP,
2948                                        token: swc_css_ast::Token::LBrace,
2949                                    },
2950                                    // TODO make the `compress_empty` option for CSS minifier and
2951                                    // remove it
2952                                    value: vec![swc_css_ast::ComponentValue::Str(Box::new(
2953                                        swc_css_ast::Str {
2954                                            span: Default::default(),
2955                                            value: atom!("placeholder"),
2956                                            raw: None,
2957                                        },
2958                                    ))],
2959                                }),
2960                            }
2961                            .into(),
2962                        )],
2963                    },
2964                    _ => return None,
2965                }
2966            }
2967        };
2968
2969        // Avoid compress potential invalid CSS
2970        if !errors.is_empty() {
2971            return None;
2972        }
2973
2974        swc_css_minifier::minify(&mut stylesheet, options.minifier);
2975
2976        let mut minified = String::new();
2977        let wr = swc_css_codegen::writer::basic::BasicCssWriter::new(
2978            &mut minified,
2979            None,
2980            swc_css_codegen::writer::basic::BasicCssWriterConfig::default(),
2981        );
2982
2983        options.codegen.minify = true;
2984
2985        let mut gen = swc_css_codegen::CodeGenerator::new(wr, options.codegen);
2986
2987        match mode {
2988            CssMinificationMode::Stylesheet => {
2989                swc_css_codegen::Emit::emit(&mut gen, &stylesheet).unwrap();
2990            }
2991            CssMinificationMode::ListOfDeclarations => {
2992                let swc_css_ast::Stylesheet { rules, .. } = &stylesheet;
2993
2994                // Because CSS is grammar free, protect for fails
2995                let Some(swc_css_ast::Rule::QualifiedRule(qualified_rule)) = rules.first() else {
2996                    return None;
2997                };
2998
2999                let swc_css_ast::QualifiedRule { block, .. } = &**qualified_rule;
3000
3001                swc_css_codegen::Emit::emit(&mut gen, &block).unwrap();
3002
3003                minified = minified[1..minified.len() - 1].to_string();
3004            }
3005            CssMinificationMode::MediaQueryList => {
3006                let swc_css_ast::Stylesheet { rules, .. } = &stylesheet;
3007
3008                // Because CSS is grammar free, protect for fails
3009                let Some(swc_css_ast::Rule::AtRule(at_rule)) = rules.first() else {
3010                    return None;
3011                };
3012
3013                let swc_css_ast::AtRule { prelude, .. } = &**at_rule;
3014
3015                swc_css_codegen::Emit::emit(&mut gen, &prelude).unwrap();
3016
3017                minified = minified.trim().to_string();
3018            }
3019        }
3020
3021        Some(minified)
3022    }
3023}
3024
3025fn create_minifier<'a, C: MinifyCss>(
3026    context_element: Option<&Element>,
3027    options: &'a MinifyOptions<C::Options>,
3028    css_minifier: &'a C,
3029) -> Minifier<'a, C> {
3030    let mut current_element = None;
3031    let mut is_pre = false;
3032
3033    if let Some(context_element) = context_element {
3034        current_element = Some(context_element.clone());
3035        is_pre = get_white_space(context_element.namespace, &context_element.tag_name)
3036            == WhiteSpace::Pre;
3037    }
3038
3039    Minifier {
3040        options,
3041
3042        current_element,
3043        latest_element: None,
3044        descendant_of_pre: is_pre,
3045        attribute_name_counter: None,
3046
3047        css_minifier,
3048    }
3049}
3050
3051pub fn minify_document_with_custom_css_minifier<C: MinifyCss>(
3052    document: &mut Document,
3053    options: &MinifyOptions<C::Options>,
3054    css_minifier: &C,
3055) {
3056    let mut minifier = create_minifier(None, options, css_minifier);
3057
3058    if options.sort_attributes {
3059        let mut attribute_name_counter = AttributeNameCounter {
3060            tree: Default::default(),
3061        };
3062
3063        document.visit_mut_with(&mut attribute_name_counter);
3064
3065        minifier.attribute_name_counter = Some(attribute_name_counter.tree);
3066    }
3067
3068    document.visit_mut_with(&mut minifier);
3069}
3070
3071pub fn minify_document_fragment_with_custom_css_minifier<C: MinifyCss>(
3072    document_fragment: &mut DocumentFragment,
3073    context_element: &Element,
3074    options: &MinifyOptions<C::Options>,
3075    css_minifier: &C,
3076) {
3077    let mut minifier = create_minifier(Some(context_element), options, css_minifier);
3078
3079    if options.sort_attributes {
3080        let mut attribute_name_counter = AttributeNameCounter {
3081            tree: Default::default(),
3082        };
3083
3084        document_fragment.visit_mut_with(&mut attribute_name_counter);
3085
3086        minifier.attribute_name_counter = Some(attribute_name_counter.tree);
3087    }
3088
3089    document_fragment.visit_mut_with(&mut minifier);
3090}
3091
3092#[cfg(feature = "default-css-minifier")]
3093pub fn minify_document(document: &mut Document, options: &MinifyOptions<CssOptions>) {
3094    minify_document_with_custom_css_minifier(document, options, &DefaultCssMinifier)
3095}
3096
3097#[cfg(feature = "default-css-minifier")]
3098pub fn minify_document_fragment(
3099    document_fragment: &mut DocumentFragment,
3100    context_element: &Element,
3101    options: &MinifyOptions<CssOptions>,
3102) {
3103    minify_document_fragment_with_custom_css_minifier(
3104        document_fragment,
3105        context_element,
3106        options,
3107        &DefaultCssMinifier,
3108    )
3109}