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