diff --git a/crates/swc_html_codegen/src/lib.rs b/crates/swc_html_codegen/src/lib.rs index c3d2fff4b309..22be7a173767 100644 --- a/crates/swc_html_codegen/src/lib.rs +++ b/crates/swc_html_codegen/src/lib.rs @@ -3,7 +3,7 @@ #![allow(clippy::match_like_matches_macro)] pub use std::fmt::Result; -use std::{iter::Peekable, str::Chars}; +use std::{borrow::Cow, iter::Peekable, str::Chars}; use swc_atoms::{js_word, JsWord}; use swc_common::Spanned; @@ -804,13 +804,23 @@ where attribute.push('='); if self.config.minify { - let minifier = minify_attribute_value(value, self.quotes); + let (minifier, quote) = minify_attribute_value(value, self.quotes); + + if let Some(quote) = quote { + attribute.push(quote); + } attribute.push_str(&minifier); + + if let Some(quote) = quote { + attribute.push(quote); + } } else { - let normalized = normalize_attribute_value(value); + let normalized = escape_string(value, true); + attribute.push('"'); attribute.push_str(&normalized); + attribute.push('"'); } } @@ -820,15 +830,11 @@ where #[emitter] fn emit_text(&mut self, n: &Text) -> Result { if self.ctx.need_escape_text { - let mut data = String::with_capacity(n.data.len()); - if self.config.minify { - data.push_str(&minify_text(&n.data)); + write_multiline_raw!(self, n.span, &minify_text(&n.data)); } else { - data.push_str(&escape_string(&n.data, false)); + write_multiline_raw!(self, n.span, &escape_string(&n.data, false)); } - - write_multiline_raw!(self, n.span, &data); } else { write_multiline_raw!(self, n.span, &n.data); } @@ -927,9 +933,20 @@ where } #[allow(clippy::unused_peekable)] -fn minify_attribute_value(value: &str, quotes: bool) -> String { +fn minify_attribute_value(value: &str, quotes: bool) -> (Cow<'_, str>, Option) { if value.is_empty() { - return "\"\"".to_string(); + return (Cow::Borrowed(value), Some('"')); + } + + // Fast-path + if !quotes + && value.chars().all(|c| match c { + '&' | '`' | '=' | '<' | '>' | '"' | '\'' => false, + c if c.is_ascii_whitespace() => false, + _ => true, + }) + { + return (Cow::Borrowed(value), None); } let mut minified = String::with_capacity(value.len()); @@ -943,7 +960,18 @@ fn minify_attribute_value(value: &str, quotes: bool) -> String { while let Some(c) = chars.next() { match c { '&' => { - minified.push_str(&minify_amp(&mut chars)); + let next = chars.next(); + + if let Some(next) = next { + if matches!(next, '#' | 'a'..='z' | 'A'..='Z') { + minified.push_str(&minify_amp(next, &mut chars)); + } else { + minified.push('&'); + minified.push(next); + } + } else { + minified.push('&'); + } continue; } @@ -969,39 +997,49 @@ fn minify_attribute_value(value: &str, quotes: bool) -> String { } if !quotes && unquoted { - return minified; + return (Cow::Owned(minified), None); } if dq > sq { - format!("'{}'", minified.replace('\'', "'")) + return (Cow::Owned(minified.replace('\'', "'")), Some('\'')); } else { - format!("\"{}\"", minified.replace('"', """)) + return (Cow::Owned(minified.replace('"', """)), Some('"')); } } -fn normalize_attribute_value(value: &str) -> String { +#[allow(clippy::unused_peekable)] +fn minify_text(value: &str) -> Cow<'_, str> { + // Fast-path if value.is_empty() { - return "\"\"".to_string(); + return Cow::Borrowed(value); } - let mut normalized = String::with_capacity(value.len() + 2); - - normalized.push('"'); - normalized.push_str(&escape_string(value, true)); - normalized.push('"'); - - normalized -} + // Fast-path + if value.chars().all(|c| match c { + '&' | '<' => false, + _ => true, + }) { + return Cow::Borrowed(value); + } -#[allow(clippy::unused_peekable)] -fn minify_text(value: &str) -> String { let mut result = String::with_capacity(value.len()); let mut chars = value.chars().peekable(); while let Some(c) = chars.next() { match c { '&' => { - result.push_str(&minify_amp(&mut chars)); + let next = chars.next(); + + if let Some(next) = next { + if matches!(next, '#' | 'a'..='z' | 'A'..='Z') { + result.push_str(&minify_amp(next, &mut chars)); + } else { + result.push('&'); + result.push(next); + } + } else { + result.push('&'); + } } '<' => { result.push_str("<"); @@ -1010,14 +1048,14 @@ fn minify_text(value: &str) -> String { } } - result + Cow::Owned(result) } -fn minify_amp(chars: &mut Peekable) -> String { +fn minify_amp(next: char, chars: &mut Peekable) -> String { let mut result = String::with_capacity(7); - match chars.next() { - Some(hash @ '#') => { + match next { + hash @ '#' => { match chars.next() { // HTML CODE // Prevent `&#38;` -> `&` @@ -1054,7 +1092,7 @@ fn minify_amp(chars: &mut Peekable) -> String { } // Named entity // Prevent `&current` -> `¤t` - Some(c @ 'a'..='z') | Some(c @ 'A'..='Z') => { + c @ 'a'..='z' | c @ 'A'..='Z' => { let mut entity_temporary_buffer = String::with_capacity(33); entity_temporary_buffer.push('&'); @@ -1091,10 +1129,7 @@ fn minify_amp(chars: &mut Peekable) -> String { } any => { result.push('&'); - - if let Some(any) = any { - result.push(any); - } + result.push(any); } } @@ -1115,7 +1150,22 @@ fn minify_amp(chars: &mut Peekable) -> String { // 4. If the algorithm was not invoked in the attribute mode, replace any // occurrences of the "<" character by the string "<", and any occurrences of // the ">" character by the string ">". -fn escape_string(value: &str, is_attribute_mode: bool) -> String { +fn escape_string(value: &str, is_attribute_mode: bool) -> Cow<'_, str> { + // Fast-path + if value.is_empty() { + return Cow::Borrowed(value); + } + + if value.chars().all(|c| match c { + '&' | '\u{00A0}' => false, + '"' if is_attribute_mode => false, + '<' if !is_attribute_mode => false, + '>' if !is_attribute_mode => false, + _ => true, + }) { + return Cow::Borrowed(value); + } + let mut result = String::with_capacity(value.len()); for c in value.chars() { @@ -1135,7 +1185,7 @@ fn escape_string(value: &str, is_attribute_mode: bool) -> String { } } - result + Cow::Owned(result) } fn is_html_tag_name(namespace: Namespace, tag_name: &JsWord) -> bool {