diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextShadowNode.cpp index 2ff46e65dc87..77ede813aedd 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextShadowNode.cpp @@ -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(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(childNode.get()); if (textShadowNode != nullptr) { diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/tests/BaseTextShadowNodeTest.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/tests/BaseTextShadowNodeTest.cpp new file mode 100644 index 000000000000..6a9e4858d1f4 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/text/tests/BaseTextShadowNodeTest.cpp @@ -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 +#include + +#include +#include +#include + +namespace facebook::react { + +namespace { + +Element rawTextElement(const char* text) { + auto rawTextProps = std::make_shared(); + rawTextProps->text = text; + return Element().props(rawTextProps); +} + +} // namespace + +TEST(BaseTextShadowNodeTest, fragmentsWithDifferentAttributes) { + ContextContainer contextContainer{}; + PropsParserContext parserContext{-1, contextContainer}; + + auto builder = simpleComponentBuilder(); + auto shadowNode = builder.build(Element().children({ + Element() + .props([]() { + auto props = std::make_shared(); + props->textAttributes.fontSize = 12; + return props; + }) + .children({ + rawTextElement("First fragment. "), + }), + Element() + .props([]() { + auto props = std::make_shared(); + 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().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