Skip to content

Commit fe2e345

Browse files
committed
fix(transformer/styled-components): remove unnecessary whitespace around block comments in CSS minification
1 parent f02b05e commit fe2e345

File tree

3 files changed

+63
-54
lines changed

3 files changed

+63
-54
lines changed

crates/oxc_transformer/src/plugins/styled_components.rs

Lines changed: 51 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,12 +1045,6 @@ fn minify_template_literal<'a>(lit: &mut TemplateLiteral<'a>, ast: AstBuilder<'a
10451045
}
10461046
// Skip line comments, but be careful not to break URLs like `https://...`
10471047
b'/' if paren_depth == 0 && (i == 0 || bytes[i - 1] != b':') => {
1048-
// Remove trailing space before comment.
1049-
// Comment will end with a line break, so space will be added again if required.
1050-
if output.last().is_some_and(|&last| last == b' ') {
1051-
output.pop();
1052-
}
1053-
10541048
let end_index =
10551049
bytes[i + 2..].iter().position(|&b| matches!(b, b'\n' | b'\r'));
10561050
if let Some(end_index) = end_index {
@@ -1067,6 +1061,17 @@ fn minify_template_literal<'a>(lit: &mut TemplateLiteral<'a>, ast: AstBuilder<'a
10671061
}
10681062
}
10691063
// Skip and compress whitespace.
1064+
//
1065+
// CSS allows removing spaces around certain delimiters without changing meaning:
1066+
// - `color: red` -> `color:red` (spaces after colons)
1067+
// - `.a { }` -> `.a{}` (spaces around braces)
1068+
// - `margin: 1px , 2px` -> `margin:1px,2px` (spaces around commas)
1069+
//
1070+
// But spaces are significant in other contexts:
1071+
// - `.a .b` - space between selectors preserved
1072+
// - `padding: 0 ${VALUE}px` - space before interpolation preserved
1073+
// - `${A} ${B}` - space between interpolations preserved
1074+
// - `.class :hover` - space before pseudo-selector preserved
10701075
_ if cur_byte.is_ascii_whitespace() => {
10711076
// Consume this whitespace and any further whitespace characters
10721077
let mut next_byte;
@@ -1078,56 +1083,23 @@ fn minify_template_literal<'a>(lit: &mut TemplateLiteral<'a>, ast: AstBuilder<'a
10781083
}
10791084
}
10801085

1081-
// Decide whether to preserve this whitespace character.
1082-
// CSS allows removing spaces around certain delimiters without changing meaning:
1083-
// - `color: red` -> `color:red` (spaces around colons)
1084-
// - `.a { }` -> `.a{}` (spaces around braces)
1085-
// - `margin: 1px , 2px` -> `margin:1px,2px` (spaces around commas)
1086-
// But spaces are significant in other contexts like selectors: `.a .b` != `.a.b`.
1087-
1088-
// Check what comes before this whitespace
1089-
if let Some(&last) = output.last() {
1090-
if matches!(last, b' ' | b':' | b'{' | b'}' | b',' | b';') {
1091-
// Always safe to remove whitespace after these characters
1092-
continue;
1093-
}
1094-
} else if quasi_index == 0 {
1095-
// We're at the start of the first quasi, so can trim leading whitespace
1096-
continue;
1097-
} else {
1098-
// We're at start of a later quasi.
1099-
// Preserve space to avoid joining with previous interpolation.
1100-
}
1101-
1102-
// Check what comes after this whitespace.
1103-
// - If we're at the end of the quasi (`next_byte == None`), and it's not the last,
1104-
// preserve the space to avoid joining with the next interpolation.
1105-
// - Remove whitespace before `{`, `}`, `,`, and `;`.
1106-
// Note: We intentionally DON'T include ':' here because spaces before colons
1107-
// are significant in CSS. ` :hover` (descendant pseudo-selector) is different
1108-
// from `:hover` (direct pseudo-selector). Example: `.parent :hover` selects any
1109-
// hovered descendant, while `.parent:hover` selects the parent when hovered.
1110-
if let Some(&next) = next_byte {
1111-
if matches!(next, b'{' | b'}' | b',' | b';') {
1112-
// Always safe to remove whitespace before these characters
1113-
continue;
1114-
}
1115-
} else if quasi_index == quasis.len() - 1 {
1116-
// We're at the end of the last quasi, so can trim trailing whitespace
1117-
continue;
1118-
} else {
1119-
// We're at end of a quasi which isn't the last one.
1120-
// Preserve space to avoid joining with next interpolation.
1121-
}
1122-
1123-
// Preserve this space character.
1124-
// Examples:
1125-
// - `padding: 0 ${VALUE}px` - space before interpolation preserved
1126-
// - `${A} ${B}` - space between interpolations preserved
1127-
// - `.class :hover` - space before pseudo-selector preserved
1128-
output.push(b' ');
1086+
// Preserve this space if it's not following a character which doesn't require one.
1087+
// Note: If a space is inserted, it may be removed again if it's followed
1088+
// by `{`, `}`, `,`, or `;`.
1089+
insert_space_if_required(&mut output, quasi_index);
11291090
continue;
11301091
}
1092+
// Remove whitespace before `{`, `}`, `,`, and `;`.
1093+
//
1094+
// Note: We intentionally DON'T include ':' here because spaces before colons
1095+
// are significant in CSS. ` :hover` (descendant pseudo-selector) is different
1096+
// from `:hover` (direct pseudo-selector). Example: `.parent :hover` selects any
1097+
// hovered descendant, while `.parent:hover` selects the parent when hovered.
1098+
b'{' | b'}' | b',' | b';' => {
1099+
if output.last() == Some(&b' ') {
1100+
output.pop();
1101+
}
1102+
}
11311103
_ => {}
11321104
}
11331105

@@ -1136,6 +1108,11 @@ fn minify_template_literal<'a>(lit: &mut TemplateLiteral<'a>, ast: AstBuilder<'a
11361108
}
11371109
}
11381110

1111+
// Remove trailing space from last quasi
1112+
if output.last() == Some(&b' ') {
1113+
output.pop();
1114+
}
1115+
11391116
// Update last quasi.
11401117
// SAFETY: Output is all picked from the original `raw` values and is guaranteed to be valid UTF-8.
11411118
let output_str = unsafe { std::str::from_utf8_unchecked(&output) };
@@ -1163,6 +1140,26 @@ fn minify_template_literal<'a>(lit: &mut TemplateLiteral<'a>, ast: AstBuilder<'a
11631140
}
11641141
}
11651142

1143+
/// Insert a space, unless preceding character makes it possible to skip.
1144+
///
1145+
/// See comments above about whitespace removal.
1146+
fn insert_space_if_required(output: &mut Vec<u8>, quasi_index: usize) {
1147+
if let Some(&last) = output.last() {
1148+
// Always safe to remove whitespace after these characters
1149+
if matches!(last, b' ' | b':' | b'{' | b'}' | b',' | b';') {
1150+
return;
1151+
}
1152+
} else {
1153+
// We're at start of a quasi. If it's the first, trim leading space.
1154+
// Otherwise, preserve space to avoid joining with previous interpolation.
1155+
if quasi_index == 0 {
1156+
return;
1157+
}
1158+
}
1159+
1160+
output.push(b' ');
1161+
}
1162+
11661163
#[cfg(test)]
11671164
mod tests {
11681165
use super::*;

tasks/transform_conformance/tests/plugin-styled-components/test/fixtures/minify-comments/input.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,13 @@ const Bar = styled.div`
1111
.a // ${123}
1212
{ color: red; }
1313
`;
14+
15+
const Qux = styled.div`
16+
color: /* blah */ red;
17+
.a /* blah */ { color: blue; }
18+
`;
19+
20+
const Bing = styled.div`
21+
color: /* ${123} */ red;
22+
.a /* ${123} */ { color: blue; }
23+
`;

tasks/transform_conformance/tests/plugin-styled-components/test/fixtures/minify-comments/output.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ import styled from 'styled-components';
22
const Button = styled.div`${x}${z}`;
33
const Foo = styled.div`.a{color:red;}`;
44
const Bar = styled.div`.a{color:red;}`;
5+
const Qux = styled.div`color:red;.a{color:blue;}`;
6+
const Bing = styled.div`color:red;.a{color:blue;}`;

0 commit comments

Comments
 (0)