Merge RawText sequences (#47278)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/47278

When we have multiple spans of text inside a <Text> element, React will emit these as separate RawText ShadowNodes. RawText shadow nodes cannot have any properties beyond the text they contain, yet our current AttributedText logic will generate a separate span for each and duplicate all the relevant properties.

This can be particularly inefficient when JSX is used to interpolate strings, e.g. `<Text styles={styles.text}>Example {i}/{count}</Text>` results in 4 raw text elements with duplicated properties.

Changelog: [General][Changed] Improved AttributedText generation for raw text nodes.

Reviewed By: NickGerleman

Differential Revision: D65134912

fbshipit-source-id: dba4296ebe9987e989760cdee66f1e076fbb7a98
This commit is contained in:
Pieter De Baets
2024-11-08 11:32:51 -08:00
committed by Facebook GitHub Bot
parent ee7b4e2763
commit 2f7957f2fd
2 changed files with 111 additions and 9 deletions
@@ -29,24 +29,33 @@ void BaseTextShadowNode::buildAttributedString(
const ShadowNode& parentNode,
AttributedString& outAttributedString,
Attachments& outAttachments) {
bool lastFragmentWasRawText = false;
for (const auto& childNode : parentNode.getChildren()) {
// RawShadowNode
auto rawTextShadowNode =
dynamic_cast<const RawTextShadowNode*>(childNode.get());
if (rawTextShadowNode != nullptr) {
auto fragment = AttributedString::Fragment{};
fragment.string = rawTextShadowNode->getConcreteProps().text;
fragment.textAttributes = baseTextAttributes;
const auto& rawText = rawTextShadowNode->getConcreteProps().text;
if (lastFragmentWasRawText) {
outAttributedString.getFragments().back().string += rawText;
} else {
auto fragment = AttributedString::Fragment{};
fragment.string = rawText;
fragment.textAttributes = baseTextAttributes;
// Storing a retaining pointer to `ParagraphShadowNode` inside
// `attributedString` causes a retain cycle (besides that fact that we
// don't need it at all). Storing a `ShadowView` instance instead of
// `ShadowNode` should properly fix this problem.
fragment.parentShadowView = shadowViewFromShadowNode(parentNode);
outAttributedString.appendFragment(fragment);
// Storing a retaining pointer to `ParagraphShadowNode` inside
// `attributedString` causes a retain cycle (besides that fact that we
// don't need it at all). Storing a `ShadowView` instance instead of
// `ShadowNode` should properly fix this problem.
fragment.parentShadowView = shadowViewFromShadowNode(parentNode);
outAttributedString.appendFragment(fragment);
lastFragmentWasRawText = true;
}
continue;
}
lastFragmentWasRawText = false;
// TextShadowNode
auto textShadowNode = dynamic_cast<const TextShadowNode*>(childNode.get());
if (textShadowNode != nullptr) {
@@ -0,0 +1,93 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <react/renderer/core/PropsParserContext.h>
#include <react/renderer/element/ComponentBuilder.h>
#include <gtest/gtest.h>
#include <react/renderer/element/Element.h>
#include <react/renderer/element/testUtils.h>
namespace facebook::react {
namespace {
Element<RawTextShadowNode> rawTextElement(const char* text) {
auto rawTextProps = std::make_shared<RawTextProps>();
rawTextProps->text = text;
return Element<RawTextShadowNode>().props(rawTextProps);
}
} // namespace
TEST(BaseTextShadowNodeTest, fragmentsWithDifferentAttributes) {
ContextContainer contextContainer{};
PropsParserContext parserContext{-1, contextContainer};
auto builder = simpleComponentBuilder();
auto shadowNode = builder.build(Element<ParagraphShadowNode>().children({
Element<TextShadowNode>()
.props([]() {
auto props = std::make_shared<TextProps>();
props->textAttributes.fontSize = 12;
return props;
})
.children({
rawTextElement("First fragment. "),
}),
Element<TextShadowNode>()
.props([]() {
auto props = std::make_shared<TextProps>();
props->textAttributes.fontSize = 24;
return props;
})
.children({
rawTextElement("Second fragment"),
}),
}));
auto baseTextAttributes = TextAttributes::defaultTextAttributes();
AttributedString output;
BaseTextShadowNode::Attachments attachments;
BaseTextShadowNode::buildAttributedString(
baseTextAttributes, *shadowNode, output, attachments);
EXPECT_EQ(output.getString(), "First fragment. Second fragment");
const auto& fragments = output.getFragments();
EXPECT_EQ(fragments.size(), 2);
EXPECT_EQ(fragments[0].textAttributes.fontSize, 12);
EXPECT_EQ(
fragments[0].parentShadowView.tag,
shadowNode->getChildren()[0]->getTag());
EXPECT_EQ(fragments[1].textAttributes.fontSize, 24);
EXPECT_EQ(
fragments[1].parentShadowView.tag,
shadowNode->getChildren()[1]->getTag());
}
TEST(BaseTextShadowNodeTest, rawTextIsMerged) {
ContextContainer contextContainer{};
PropsParserContext parserContext{-1, contextContainer};
auto builder = simpleComponentBuilder();
auto shadowNode = builder.build(Element<TextShadowNode>().children({
rawTextElement("Hello "),
rawTextElement("World"),
}));
auto baseTextAttributes = TextAttributes::defaultTextAttributes();
AttributedString output;
BaseTextShadowNode::Attachments attachments;
BaseTextShadowNode::buildAttributedString(
baseTextAttributes, *shadowNode, output, attachments);
EXPECT_EQ(output.getString(), "Hello World");
EXPECT_EQ(output.getFragments().size(), 1);
}
} // namespace facebook::react