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 ("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 "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 if attribute_info.deprecated == Some(true) {
675 return true;
676 }
677
678 if element.namespace == Namespace::SVG {
681 return true;
682 }
683
684 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 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 !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 _ 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 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 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 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 Some(Display::None) | Some(Display::Inline) => {
1480 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 Some(_) => false,
1538 None => {
1539 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 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 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 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 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 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 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 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 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 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 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 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 &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 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 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 let mut context_element = None;
2150
2151 let mut document_or_document_fragment = match mode {
2152 HtmlMinificationMode::ConditionalComments => {
2153 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 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 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 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 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(¤t_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 ¤t_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 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 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 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 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}