feat(transformer/react): implement fixup_whitespace_and_decode_entities (#1091)

This commit is contained in:
Boshen 2023-10-29 18:11:51 +08:00 committed by GitHub
parent fe1bbaf0d4
commit 262631da62
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 60 additions and 22 deletions

View file

@ -5,7 +5,10 @@ use std::rc::Rc;
use oxc_allocator::Vec;
use oxc_ast::{ast::*, AstBuilder};
use oxc_span::{Atom, SPAN};
use oxc_syntax::xml_entities::XML_ENTITIES;
use oxc_syntax::{
identifier::{is_irregular_whitespace, is_line_terminator},
xml_entities::XML_ENTITIES,
};
pub use self::options::{ReactJsxOptions, ReactJsxRuntime};
@ -408,7 +411,7 @@ impl<'a> ReactJsx<'a> {
) -> Expression<'a> {
match value {
Some(JSXAttributeValue::String(s)) => {
let jsx_text = Self::decode_jsx_text(&s.value);
let jsx_text = Self::decode_entities(&s.value);
let literal = StringLiteral::new(s.span, jsx_text.into());
self.ast.literal_string_expression(literal)
}
@ -461,24 +464,66 @@ impl<'a> ReactJsx<'a> {
}
fn transform_jsx_text(&self, text: &JSXString) -> Option<Expression<'a>> {
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::<std::vec::Vec<_>>()
.join(" ");
let s = StringLiteral::new(SPAN, text.into());
Self::fixup_whitespace_and_decode_entities(text.value.as_str()).map(|s| {
let s = StringLiteral::new(SPAN, s.into());
self.ast.literal_string_expression(s)
})
}
/// JSX trims whitespace at the end and beginning of lines, except that the
/// start/end of a tag is considered a start/end of a line only if that line is
/// on the same line as the closing tag. See examples in
/// tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx
/// See also https://www.w3.org/TR/html4/struct/text.html#h-9.1 and https://www.w3.org/TR/CSS2/text.html#white-space-model
///
/// An equivalent algorithm would be:
/// - If there is only one line, return it.
/// - If there is only whitespace (but multiple lines), return `undefined`.
/// - Split the text into lines.
/// - 'trimRight' the first line, 'trimLeft' the last line, 'trim' middle lines.
/// - Decode entities on each line (individually).
/// - Remove empty lines and join the rest with " ".
///
/// <https://github.com/microsoft/TypeScript/blob/f0374ce2a9c465e27a15b7fa4a347e2bd9079450/src/compiler/transformers/jsx.ts#L557-L608>
fn fixup_whitespace_and_decode_entities(text: &str) -> Option<String> {
let mut acc: Option<String> = None;
let mut first_non_whitespace: Option<usize> = Some(0);
let mut last_non_whitespace: Option<usize> = None;
let mut i: usize = 0;
for c in text.chars() {
if is_line_terminator(c) {
if let (Some(first), Some(last)) = (first_non_whitespace, last_non_whitespace) {
acc = Some(Self::add_line_of_jsx_text(acc, &text[first..=last]));
}
first_non_whitespace = None;
} else if c != ' ' && !is_irregular_whitespace(c) {
last_non_whitespace = Some(i);
if first_non_whitespace.is_none() {
first_non_whitespace.replace(i);
}
}
i += c.len_utf8();
}
if let Some(first) = first_non_whitespace {
Some(Self::add_line_of_jsx_text(acc, &text[first..]))
} else {
acc
}
}
fn add_line_of_jsx_text(acc: Option<String>, trimmed_line: &str) -> String {
let decoded = Self::decode_entities(trimmed_line);
if let Some(acc) = acc {
format!("{acc} {decoded}")
} else {
decoded
}
}
/// * Replace entities like "&nbsp;", "&#123;", and "&#xDEADBEEF;" with the characters they encode.
/// * See https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references
/// Code adapted from <https://github.com/microsoft/TypeScript/blob/514f7e639a2a8466c075c766ee9857a30ed4e196/src/compiler/transformers/jsx.ts#L617C1-L635>
fn decode_jsx_text(s: &str) -> String {
fn decode_entities(s: &str) -> String {
let mut buffer = vec![];
let mut chars = s.bytes().enumerate();
let mut prev = 0;

View file

@ -1,4 +1,4 @@
Passed: 234/1083
Passed: 241/1083
# All Passed:
* babel-plugin-transform-numeric-separator
@ -804,7 +804,7 @@ Passed: 234/1083
* regression/11061/input.mjs
* variable-declaration/non-null-in-optional-chain/input.ts
# babel-plugin-transform-react-jsx (85/172)
# babel-plugin-transform-react-jsx (92/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
@ -847,10 +847,6 @@ Passed: 234/1083
* react/should-disallow-spread-children/input.js
* react/should-disallow-valueless-key/input.js
* react/should-disallow-xml-namespacing/input.js
* react/should-escape-xhtml-jsxtext/input.js
* 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-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
@ -870,10 +866,7 @@ Passed: 234/1083
* react-automatic/should-disallow-spread-children/input.js
* react-automatic/should-disallow-valueless-key/input.js
* react-automatic/should-disallow-xml-namespacing/input.js
* react-automatic/should-escape-xhtml-jsxtext/input.js
* 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-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