From 5b70233400a5b67cf51650bebbd59dcd0699ba78 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Tue, 12 Jul 2022 06:12:32 +0300 Subject: [PATCH] fix(css/codegen): Fix sourcemap of multibyte characters (#5183) --- Cargo.lock | 1 + crates/swc_css_codegen/Cargo.toml | 17 +-- crates/swc_css_codegen/src/lib.rs | 77 ++++++----- crates/swc_css_codegen/src/writer/basic.rs | 120 ++++++++++++------ .../tests/fixture/values/string/input.css | 2 +- .../tests/fixture/values/string/output.css | 2 +- .../fixture/values/string/output.min.css | 2 +- 7 files changed, 141 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2768098c3ac..fc744ef92a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3174,6 +3174,7 @@ version = "0.108.0" dependencies = [ "auto_impl", "bitflags", + "rustc-hash", "serde", "swc_atoms", "swc_common", diff --git a/crates/swc_css_codegen/Cargo.toml b/crates/swc_css_codegen/Cargo.toml index 3edebbc4ce9..169af179904 100644 --- a/crates/swc_css_codegen/Cargo.toml +++ b/crates/swc_css_codegen/Cargo.toml @@ -15,16 +15,17 @@ bench = false [dependencies] auto_impl = "0.5.0" bitflags = "1.3.2" +rustc-hash = "1.1.0" serde = "1.0.127" -swc_atoms = {version = "0.2.7", path = "../swc_atoms"} -swc_common = { version = "0.23.0", path = "../swc_common"} -swc_css_ast = {version = "0.98.0", path = "../swc_css_ast"} -swc_css_codegen_macros = {version = "0.2.0", path = "../swc_css_codegen_macros"} +swc_atoms = { version = "0.2.7", path = "../swc_atoms" } +swc_common = { version = "0.23.0", path = "../swc_common" } +swc_css_ast = { version = "0.98.0", path = "../swc_css_ast" } +swc_css_codegen_macros = { version = "0.2.0", path = "../swc_css_codegen_macros" } [dev-dependencies] swc_common = { version = "0.23.0", path = "../swc_common", features = [ "sourcemap", -]} -swc_css_parser = {version = "0.107.0", path = "../swc_css_parser"} -swc_css_visit = {version = "0.97.0", path = "../swc_css_visit"} -testing = {version = "0.25.0", path = "../testing"} +] } +swc_css_parser = { version = "0.107.0", path = "../swc_css_parser" } +swc_css_visit = { version = "0.97.0", path = "../swc_css_visit" } +testing = { version = "0.25.0", path = "../testing" } diff --git a/crates/swc_css_codegen/src/lib.rs b/crates/swc_css_codegen/src/lib.rs index 459fa9ae6cb..5698a3eac42 100644 --- a/crates/swc_css_codegen/src/lib.rs +++ b/crates/swc_css_codegen/src/lib.rs @@ -989,7 +989,8 @@ where #[emitter] fn emit_important_flag(&mut self, n: &ImportantFlag) -> Result { - let mut value = String::new(); + // `!` + `important` + let mut value = String::with_capacity(10); value.push('!'); @@ -1133,7 +1134,7 @@ where #[emitter] fn emit_hex_color(&mut self, n: &HexColor) -> Result { - let mut hex_color = String::new(); + let mut hex_color = String::with_capacity(5); hex_color.push('#'); @@ -1245,7 +1246,7 @@ where match &n.token { Token::AtKeyword { raw, .. } => { - let mut at_keyword = String::new(); + let mut at_keyword = String::with_capacity(1 + raw.len()); at_keyword.push('@'); at_keyword.push_str(raw); @@ -1271,7 +1272,7 @@ where write_raw!(self, span, raw); } Token::Percentage { raw, .. } => { - let mut percentage = String::new(); + let mut percentage = String::with_capacity(raw.len() + 1); percentage.push_str(raw); percentage.push('%'); @@ -1283,7 +1284,7 @@ where raw_unit, .. } => { - let mut dimension = String::new(); + let mut dimension = String::with_capacity(raw_value.len() + raw_unit.len()); dimension.push_str(raw_value); dimension.push_str(raw_unit); @@ -1294,7 +1295,7 @@ where write_raw!(self, span, raw); } Token::Function { raw, .. } => { - let mut function = String::new(); + let mut function = String::with_capacity(raw.len() + 1); function.push_str(raw); function.push('('); @@ -1314,7 +1315,9 @@ where after, .. } => { - let mut url = String::new(); + let mut url = String::with_capacity( + raw_name.len() + before.len() + raw_value.len() + after.len() + 2, + ); url.push_str(raw_name); url.push('('); @@ -1330,7 +1333,7 @@ where raw_value, .. } => { - let mut bad_url = String::new(); + let mut bad_url = String::with_capacity(raw_name.len() + raw_value.len() + 2); bad_url.push_str(raw_name); bad_url.push('('); @@ -1355,7 +1358,7 @@ where write_raw!(self, span, ":"); } Token::Hash { raw, .. } => { - let mut hash = String::new(); + let mut hash = String::with_capacity(raw.len() + 1); hash.push('#'); hash.push_str(raw); @@ -1381,7 +1384,7 @@ where match token { Token::AtKeyword { raw, .. } => { - let mut at_keyword = String::new(); + let mut at_keyword = String::with_capacity(raw.len() + 1); at_keyword.push('@'); at_keyword.push_str(raw); @@ -1407,7 +1410,7 @@ where write_raw!(self, span, raw); } Token::Percentage { raw, .. } => { - let mut percentage = String::new(); + let mut percentage = String::with_capacity(raw.len() + 1); percentage.push_str(raw); percentage.push('%'); @@ -1419,7 +1422,7 @@ where raw_unit, .. } => { - let mut dimension = String::new(); + let mut dimension = String::with_capacity(raw_value.len() + raw_unit.len()); dimension.push_str(raw_value); dimension.push_str(raw_unit); @@ -1430,7 +1433,7 @@ where write_raw!(self, span, raw); } Token::Function { raw, .. } => { - let mut function = String::new(); + let mut function = String::with_capacity(raw.len() + 1); function.push_str(raw); function.push('('); @@ -1450,7 +1453,9 @@ where after, .. } => { - let mut url = String::new(); + let mut url = String::with_capacity( + raw_name.len() + before.len() + raw_value.len() + after.len() + 2, + ); url.push_str(raw_name); url.push('('); @@ -1466,7 +1471,7 @@ where raw_value, .. } => { - let mut bad_url = String::new(); + let mut bad_url = String::with_capacity(raw_name.len() + raw_value.len() + 2); bad_url.push_str(raw_name); bad_url.push('('); @@ -1491,7 +1496,7 @@ where write_raw!(self, span, ":"); } Token::Hash { raw, .. } => { - let mut hash = String::new(); + let mut hash = String::with_capacity(raw.len() + 1); hash.push('#'); hash.push_str(raw); @@ -1543,23 +1548,21 @@ where #[emitter] fn emit_url_value_raw(&mut self, n: &UrlValueRaw) -> Result { - let mut url = String::new(); - - if !self.config.minify { - url.push_str(&n.before); - } - if self.config.minify { + let mut url = String::with_capacity(n.value.len()); + url.push_str(&n.value); + + write_str!(self, n.span, &url); } else { + let mut url = String::with_capacity(n.before.len() + n.raw.len() + n.after.len()); + + url.push_str(&n.before); url.push_str(&n.raw); - } - - if !self.config.minify { url.push_str(&n.after); - } - write_str!(self, n.span, &url); + write_str!(self, n.span, &url); + } } #[emitter] @@ -1572,7 +1575,15 @@ where #[emitter] fn emit_unicode_range(&mut self, n: &UnicodeRange) -> Result { - let mut value = String::new(); + let mut value = String::with_capacity( + n.start.len() + + if let Some(end) = &n.end { + end.len() + 1 + } else { + 0 + } + + 2, + ); value.push(n.prefix); value.push('+'); @@ -1785,7 +1796,7 @@ where #[emitter] fn emit_an_plus_b_notation(&mut self, n: &AnPlusBNotation) -> Result { if self.config.minify { - let mut an_plus_b_minified = String::new(); + let mut an_plus_b_minified = String::with_capacity(4); if let Some(a) = &n.a { if *a == -1 { @@ -1807,7 +1818,7 @@ where write_raw!(self, n.span, &an_plus_b_minified); } else { - let mut an_plus_b = String::new(); + let mut an_plus_b = String::with_capacity(4); if let Some(a_raw) = &n.a_raw { an_plus_b.push_str(a_raw); @@ -2021,7 +2032,7 @@ fn minify_hex_color(value: &str) -> String { if chars[0] == chars[1] && chars[2] == chars[3] && chars[4] == chars[5] { // 6 -> 3 or 8 -> 3 if length == 6 || chars[6] == b'f' && chars[7] == b'f' { - let mut minified = String::new(); + let mut minified = String::with_capacity(3); minified.push((chars[0] as char).to_ascii_lowercase()); minified.push((chars[2] as char).to_ascii_lowercase()); @@ -2031,7 +2042,7 @@ fn minify_hex_color(value: &str) -> String { } // 8 -> 4 else if length == 8 && chars[6] == chars[7] { - let mut minified = String::new(); + let mut minified = String::with_capacity(4); minified.push((chars[0] as char).to_ascii_lowercase()); minified.push((chars[2] as char).to_ascii_lowercase()); @@ -2047,7 +2058,7 @@ fn minify_hex_color(value: &str) -> String { } fn minify_string(value: &str) -> String { - let mut minified = String::new(); + let mut minified = String::with_capacity(value.len()); let mut dq = 0; let mut sq = 0; diff --git a/crates/swc_css_codegen/src/writer/basic.rs b/crates/swc_css_codegen/src/writer/basic.rs index 1d23e35312c..7b53fd1fd4a 100644 --- a/crates/swc_css_codegen/src/writer/basic.rs +++ b/crates/swc_css_codegen/src/writer/basic.rs @@ -1,5 +1,6 @@ use std::fmt::{Result, Write}; +use rustc_hash::FxHashSet; use swc_common::{BytePos, LineCol, Span}; use super::CssWriter; @@ -55,6 +56,9 @@ where linefeed: &'a str, srcmap: Option<&'a mut Vec<(BytePos, LineCol)>>, + srcmap_done: FxHashSet<(BytePos, u32, u32)>, + /// Used to avoid including whitespaces created by indention. + pending_srcmap: Option, config: BasicCssWriterConfig, @@ -92,14 +96,35 @@ where srcmap, w: writer, + pending_srcmap: Default::default(), + srcmap_done: Default::default(), } } + fn write_indent_string(&mut self) -> Result { + for _ in 0..(self.config.indent_width * self.indent_level as i32) { + self.raw_write(self.indent_type)?; + } + + Ok(()) + } + + fn raw_write(&mut self, data: &str) -> Result { + self.w.write_str(data)?; + self.col += data.chars().count(); + + Ok(()) + } + fn write(&mut self, span: Option, data: &str) -> Result { if !data.is_empty() { if self.line_start { self.write_indent_string()?; self.line_start = false; + + if let Some(pending) = self.pending_srcmap.take() { + self.srcmap(pending); + } } if let Some(span) = span { @@ -120,30 +145,23 @@ where Ok(()) } - fn write_indent_string(&mut self) -> Result { - for _ in 0..(self.config.indent_width * self.indent_level as i32) { - self.raw_write(self.indent_type)?; + fn srcmap(&mut self, byte_pos: BytePos) { + if byte_pos.is_dummy() { + return; } - Ok(()) - } - - fn raw_write(&mut self, data: &str) -> Result { - self.w.write_str(data)?; - self.col += data.chars().count(); - - Ok(()) - } - - fn srcmap(&mut self, byte_pos: BytePos) { if let Some(ref mut srcmap) = self.srcmap { - srcmap.push(( - byte_pos, - LineCol { + if self + .srcmap_done + .insert((byte_pos, self.line as _, self.col as _)) + { + let loc = LineCol { line: self.line as _, col: self.col as _, - }, - )) + }; + + srcmap.push((byte_pos, loc)); + } } } } @@ -157,11 +175,17 @@ where } fn write_newline(&mut self) -> Result { + let pending = self.pending_srcmap.take(); + if !self.line_start { self.raw_write(self.linefeed)?; self.line += 1; self.col = 0; self.line_start = true; + + if let Some(pending) = pending { + self.srcmap(pending) + } } Ok(()) @@ -181,27 +205,24 @@ where fn write_str(&mut self, span: Span, s: &str) -> Result { if !s.is_empty() { - let mut lines = s.split('\n').peekable(); - let mut lo_byte_pos = span.lo(); + if !span.is_dummy() { + self.srcmap(span.lo()) + } - while let Some(line) = lines.next() { - if !span.is_dummy() { - self.srcmap(lo_byte_pos) - } + self.write(None, s)?; - self.raw_write(line)?; + let line_start_of_s = compute_line_starts(s); - if lines.peek().is_some() { - self.raw_write("\n")?; - self.line += 1; - self.col = 0; + if line_start_of_s.len() > 1 { + self.line = self.line + line_start_of_s.len() - 1; - if !span.is_dummy() { - lo_byte_pos = lo_byte_pos + BytePos((line.len() + 1) as u32); - } - } else if !span.is_dummy() { - self.srcmap(span.hi()); - } + let last_line_byte_index = line_start_of_s.last().cloned().unwrap_or(0); + + self.col = s[last_line_byte_index..].chars().count(); + } + + if !span.is_dummy() { + self.srcmap(span.hi()) } } @@ -221,3 +242,30 @@ where self.indent_level -= 1; } } + +fn compute_line_starts(s: &str) -> Vec { + let mut res = vec![]; + let mut line_start = 0; + let mut chars = s.char_indices().peekable(); + + while let Some((pos, c)) = chars.next() { + match c { + '\r' => { + if let Some(&(_, '\n')) = chars.peek() { + let _ = chars.next(); + } + } + + '\n' => { + res.push(line_start); + line_start = pos + 1; + } + + _ => {} + } + } + + // Last line. + res.push(line_start); + res +} diff --git a/crates/swc_css_codegen/tests/fixture/values/string/input.css b/crates/swc_css_codegen/tests/fixture/values/string/input.css index 450e1db962b..c84cbca7a49 100644 --- a/crates/swc_css_codegen/tests/fixture/values/string/input.css +++ b/crates/swc_css_codegen/tests/fixture/values/string/input.css @@ -1,5 +1,5 @@ div { - content: '⬇️'; + content: '⬇'; content: "😀"; content: "\'test\'"; content: "\\'test\'"; diff --git a/crates/swc_css_codegen/tests/fixture/values/string/output.css b/crates/swc_css_codegen/tests/fixture/values/string/output.css index 62788772c1c..3b4ddcc4ef6 100644 --- a/crates/swc_css_codegen/tests/fixture/values/string/output.css +++ b/crates/swc_css_codegen/tests/fixture/values/string/output.css @@ -1,5 +1,5 @@ div { - content: '⬇️'; + content: '⬇'; content: "😀"; content: "\'test\'"; content: "\\'test\'"; diff --git a/crates/swc_css_codegen/tests/fixture/values/string/output.min.css b/crates/swc_css_codegen/tests/fixture/values/string/output.min.css index 114978df8fa..996599ae6b7 100644 --- a/crates/swc_css_codegen/tests/fixture/values/string/output.min.css +++ b/crates/swc_css_codegen/tests/fixture/values/string/output.min.css @@ -1 +1 @@ -div{content:"⬇️";content:"😀";content:"'test'";content:"\\'test'";content:"\\\\'test'";content:'"string" is string';content:"'string' is string";content:'"string" is string';content:"'string' is string";content:'"string" is string';content:"'string' is string";content:"'string' is \"string\"";content:"'string' is \"string\"";content:"'test' 'test'";content:'"test" "test"';content:"'test'";content:"'test'";content:'"test"';content:"'test'";content:"'test'";content:'"test"';content:'"string"';content:"'string'";content:"This string has a \a line break in it.";content:"This string has a \a line break in it.";content:"This string has a \a line break in it.";content:"This string has a \a line break in it.";content:'"test" "test" "test" \'test\'';content:'"test" "test" "test" \'test\'';content:"'test' 'test' 'test' \"test\"";content:'"test" "test" "test" \'test\'';content:"'test' 'test' 'test' \"test\"";content:"'test' 'test' 'test' \"test\"";content:"\\'test\\' \\'test\\' \\'test\\' \\\"test\\\"";background:url("http://example.com/foo'bar.jpg");background:url('http://example.com/foo"bar.jpg');background:url("http://example.com/foo'bar.jpg");background:url('http://example.com/foo"bar.jpg')}div{grid-template:[header-top]"a a a"[header-bottom][main-top]"b b b"1fr[main-bottom]/auto 1fr auto}.other{grid-template:[header-left]"head head"30px[header-right][main-left]"nav main"1fr[main-right][footer-left]"nav foot"30px[footer-right]/120px 1fr}.prop{prop:name"test"}.foo{content:"string is string"}.foo{content:"string is string"} +div{content:"⬇";content:"😀";content:"'test'";content:"\\'test'";content:"\\\\'test'";content:'"string" is string';content:"'string' is string";content:'"string" is string';content:"'string' is string";content:'"string" is string';content:"'string' is string";content:"'string' is \"string\"";content:"'string' is \"string\"";content:"'test' 'test'";content:'"test" "test"';content:"'test'";content:"'test'";content:'"test"';content:"'test'";content:"'test'";content:'"test"';content:'"string"';content:"'string'";content:"This string has a \a line break in it.";content:"This string has a \a line break in it.";content:"This string has a \a line break in it.";content:"This string has a \a line break in it.";content:'"test" "test" "test" \'test\'';content:'"test" "test" "test" \'test\'';content:"'test' 'test' 'test' \"test\"";content:'"test" "test" "test" \'test\'';content:"'test' 'test' 'test' \"test\"";content:"'test' 'test' 'test' \"test\"";content:"\\'test\\' \\'test\\' \\'test\\' \\\"test\\\"";background:url("http://example.com/foo'bar.jpg");background:url('http://example.com/foo"bar.jpg');background:url("http://example.com/foo'bar.jpg");background:url('http://example.com/foo"bar.jpg')}div{grid-template:[header-top]"a a a"[header-bottom][main-top]"b b b"1fr[main-bottom]/auto 1fr auto}.other{grid-template:[header-left]"head head"30px[header-right][main-left]"nav main"1fr[main-right][footer-left]"nav foot"30px[footer-right]/120px 1fr}.prop{prop:name"test"}.foo{content:"string is string"}.foo{content:"string is string"}