diff --git a/crates/oxc_codegen/src/gen.rs b/crates/oxc_codegen/src/gen.rs index 3eb95bf4c..f149d3ddb 100644 --- a/crates/oxc_codegen/src/gen.rs +++ b/crates/oxc_codegen/src/gen.rs @@ -1076,6 +1076,9 @@ fn print_str(s: &str, p: &mut Codegen<{ MINIFY }>) { // LS => p.print_str(b"\\u2028"), PS => p.print_str(b"\\u2029"), + '\u{a0}' => { + p.print_str(b"\\xA0"); + } _ => p.print_str(c.escape_default().to_string().as_bytes()), } } diff --git a/crates/oxc_transformer/src/react_jsx/mod.rs b/crates/oxc_transformer/src/react_jsx/mod.rs index eb3d84326..e3718b8cf 100644 --- a/crates/oxc_transformer/src/react_jsx/mod.rs +++ b/crates/oxc_transformer/src/react_jsx/mod.rs @@ -442,19 +442,7 @@ impl<'a> ReactJsx<'a> { fn transform_jsx_child(&mut self, child: &JSXChild<'a>) -> Option> { match child { - JSXChild::Text(text) => { - let text = text.value.trim(); - (!text.trim().is_empty()).then(|| { - let text = text - .split(char::is_whitespace) - .map(str::trim) - .filter(|c| !c.is_empty()) - .collect::>() - .join(" "); - let s = StringLiteral::new(SPAN, text.into()); - self.ast.literal_string_expression(s) - }) - } + JSXChild::Text(text) => self.transform_jsx_text(text), JSXChild::ExpressionContainer(e) => match &e.expression { JSXExpression::Expression(e) => Some(self.ast.copy(e)), JSXExpression::EmptyExpression(_) => None, @@ -467,4 +455,53 @@ impl<'a> ReactJsx<'a> { } } } + + fn transform_jsx_text(&self, text: &JSXText) -> Option> { + let text = text.value.trim(); + (!text.trim().is_empty()).then(|| { + let text = text + .split(char::is_whitespace) + .map(str::trim) + .filter(|c| !c.is_empty()) + .map(Self::decode_jsx_text) + .collect::>() + .join(" "); + let s = StringLiteral::new(SPAN, text.into()); + self.ast.literal_string_expression(s) + }) + } + + /// * Replace entities like " ", "{", and "�" with the characters they encode. + /// * See https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references + /// Code adapted from + fn decode_jsx_text(s: &str) -> String { + let mut buffer = vec![]; + let mut chars = s.bytes().enumerate(); + let mut prev = 0; + while let Some((i, c)) = chars.next() { + if c == b'&' { + let start = i; + let mut end = None; + for (j, c) in chars.by_ref() { + if c == b';' { + end.replace(j); + break; + } + } + if let Some(end) = end { + let word = &s[start + 1..end]; + buffer.extend_from_slice(s[prev..start].as_bytes()); + prev = end + 1; + match word { + "amp" => buffer.extend_from_slice(b"&"), + "nbsp" => buffer.extend_from_slice("\u{a0}".as_bytes()), + _ => {} + } + } + } + } + buffer.extend_from_slice(s[prev..].as_bytes()); + // Safety: The buffer is constructed from valid utf chars. + unsafe { String::from_utf8_unchecked(buffer) } + } } diff --git a/tasks/transform_conformance/babel.snap.md b/tasks/transform_conformance/babel.snap.md index dfc10125c..68a379fe2 100644 --- a/tasks/transform_conformance/babel.snap.md +++ b/tasks/transform_conformance/babel.snap.md @@ -1,4 +1,4 @@ -Passed: 226/1083 +Passed: 229/1083 # All Passed: * babel-plugin-transform-numeric-separator @@ -804,7 +804,7 @@ Passed: 226/1083 * regression/11061/input.mjs * variable-declaration/non-null-in-optional-chain/input.ts -# babel-plugin-transform-react-jsx (77/172) +# babel-plugin-transform-react-jsx (80/172) * autoImport/after-polyfills-compiled-to-cjs/input.mjs * autoImport/after-polyfills-script-not-supported/input.js * autoImport/auto-import-react-source-type-module/input.js @@ -853,12 +853,10 @@ Passed: 226/1083 * react/should-escape-xhtml-jsxtext-babel-7/input.js * react/should-handle-attributed-elements/input.js * react/should-not-strip-nbsp-even-coupled-with-other-whitespace/input.js -* react/should-not-strip-tags-with-a-single-child-of-nbsp/input.js * react/should-support-xml-namespaces-if-flag/input.js * react/should-throw-error-namespaces-if-not-flag/input.js * react/should-warn-when-importSource-is-set/input.js * react/should-warn-when-importSource-pragma-is-set/input.js -* react/weird-symbols/input.js * react/wraps-props-in-react-spread-for-first-spread-attributes-babel-7/input.js * react/wraps-props-in-react-spread-for-last-spread-attributes-babel-7/input.js * react/wraps-props-in-react-spread-for-middle-spread-attributes-babel-7/input.js @@ -880,7 +878,6 @@ Passed: 226/1083 * react-automatic/should-escape-xhtml-jsxtext-babel-7/input.js * react-automatic/should-handle-attributed-elements/input.js * react-automatic/should-not-strip-nbsp-even-coupled-with-other-whitespace/input.js -* react-automatic/should-not-strip-tags-with-a-single-child-of-nbsp/input.js * react-automatic/should-properly-handle-comments-between-props/input.js * react-automatic/should-throw-error-namespaces-if-not-flag/input.js * react-automatic/should-throw-when-filter-is-specified/input.js