#include "T3MarkdownTextShadowNode.h" #include "T3MarkdownTextRunShadowNode.h" #include #import #include #include namespace facebook::react { static constexpr Float ParagraphStyleEncodingOffset = 1000; static constexpr auto FileAttachmentNativeIdPrefix = "t3-file:"; static constexpr auto SkillAttachmentNativeIdPrefix = "t3-skill:"; static void applyParagraphStyles( NSMutableAttributedString *attributedString, const std::vector &styleRanges) { for (const auto &styleRange : styleRanges) { if (styleRange.length == 0 || styleRange.location >= attributedString.length) { continue; } const NSRange markerRange = NSMakeRange( styleRange.location, MIN(styleRange.length, attributedString.length - styleRange.location)); const NSRange paragraphRange = [attributedString.string paragraphRangeForRange:markerRange]; const NSParagraphStyle *existingStyle = [attributedString attribute:NSParagraphStyleAttributeName atIndex:paragraphRange.location effectiveRange:nil]; NSMutableParagraphStyle *paragraphStyle = existingStyle ? [existingStyle mutableCopy] : [NSMutableParagraphStyle new]; paragraphStyle.firstLineHeadIndent = styleRange.firstLineHeadIndent; paragraphStyle.headIndent = styleRange.headIndent; paragraphStyle.paragraphSpacing = styleRange.paragraphSpacing; paragraphStyle.tabStops = @[ [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentLeft location:styleRange.headIndent options:@{}] ]; paragraphStyle.defaultTabInterval = styleRange.headIndent; [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:paragraphRange]; } } static void applyAttachments( NSMutableAttributedString *attributedString, const std::vector &attachmentRanges) { for (const auto &attachmentRange : attachmentRanges) { if (attachmentRange.length == 0 || attachmentRange.location >= attributedString.length) { continue; } NSTextAttachment *attachment = [[NSTextAttachment alloc] init]; attachment.image = [[UIImage alloc] init]; const CGFloat attachmentSize = T3MarkdownTextAttachmentSize(attachmentRange); attachment.bounds = CGRectMake( 0, T3MarkdownTextAttachmentBaselineOffset(attachmentRange), attachmentSize, attachmentSize); const NSRange range = NSMakeRange( attachmentRange.location, MIN(attachmentRange.length, attributedString.length - attachmentRange.location)); NSAttributedString *attachmentString = [NSAttributedString attributedStringWithAttachment:attachment]; [attributedString replaceCharactersInRange:range withAttributedString:attachmentString]; } } T3MarkdownTextShadowNode::T3MarkdownTextShadowNode( const ShadowNode& sourceShadowNode, const ShadowNodeFragment& fragment ) : ConcreteViewShadowNode(sourceShadowNode, fragment) { }; Size T3MarkdownTextShadowNode::measureContent( const LayoutContext& layoutContext, const LayoutConstraints& layoutConstraints) const { const auto &baseProps = getConcreteProps(); auto baseTextAttributes = TextAttributes::defaultTextAttributes(); baseTextAttributes.backgroundColor = baseProps.backgroundColor; baseTextAttributes.allowFontScaling = baseProps.allowFontScaling; Float fontSizeMultiplier = 1.0; if (baseTextAttributes.allowFontScaling) { fontSizeMultiplier = layoutContext.fontSizeMultiplier; } auto baseAttributedString = AttributedString{}; auto paragraphStyleRanges = std::vector{}; auto attachmentRanges = std::vector{}; size_t utf16Offset = 0; const auto &children = getChildren(); for (size_t i = 0; i < children.size(); i++) { const auto child = children[i].get(); if (auto textViewChild = dynamic_cast(child)) { auto &props = textViewChild->getConcreteProps(); auto fragment = AttributedString::Fragment{}; auto textAttributes = TextAttributes::defaultTextAttributes(); textAttributes.allowFontScaling = baseProps.allowFontScaling; textAttributes.backgroundColor = props.backgroundColor; textAttributes.fontSize = props.fontSize * fontSizeMultiplier; textAttributes.lineHeight = props.lineHeight * fontSizeMultiplier; textAttributes.foregroundColor = props.color; const bool hasParagraphStyle = props.shadowRadius >= ParagraphStyleEncodingOffset; if (!hasParagraphStyle) { textAttributes.textShadowColor = props.shadowColor; textAttributes.textShadowOffset = props.shadowOffset; textAttributes.textShadowRadius = props.shadowRadius; } textAttributes.letterSpacing = props.letterSpacing; textAttributes.textDecorationColor = props.textDecorationColor; textAttributes.fontFamily = props.fontFamily; if (props.fontStyle == T3MarkdownTextRunFontStyle::Italic) { textAttributes.fontStyle = FontStyle::Italic; } else { textAttributes.fontStyle = FontStyle::Normal; } if (props.fontWeight == T3MarkdownTextRunFontWeight::Bold) { textAttributes.fontWeight = FontWeight::Bold; } else if (props.fontWeight == T3MarkdownTextRunFontWeight::UltraLight) { textAttributes.fontWeight = FontWeight::UltraLight; } else if (props.fontWeight == T3MarkdownTextRunFontWeight::Light) { textAttributes.fontWeight = FontWeight::Light; } else if (props.fontWeight == T3MarkdownTextRunFontWeight::Medium) { textAttributes.fontWeight = FontWeight::Medium; } else if (props.fontWeight == T3MarkdownTextRunFontWeight::Semibold) { textAttributes.fontWeight = FontWeight::Semibold; } else if (props.fontWeight == T3MarkdownTextRunFontWeight::Heavy) { textAttributes.fontWeight = FontWeight::Heavy; } else { textAttributes.fontWeight = FontWeight::Regular; } if (props.textDecorationLine == T3MarkdownTextRunTextDecorationLine::LineThrough) { textAttributes.textDecorationLineType = TextDecorationLineType::Strikethrough; } else if (props.textDecorationLine == T3MarkdownTextRunTextDecorationLine::Underline) { textAttributes.textDecorationLineType = TextDecorationLineType::Underline; } else { textAttributes.textDecorationLineType = TextDecorationLineType::None; } if (props.textDecorationStyle == T3MarkdownTextRunTextDecorationStyle::Solid) { textAttributes.textDecorationStyle = TextDecorationStyle::Solid; } else if (props.textDecorationStyle == T3MarkdownTextRunTextDecorationStyle::Dotted) { textAttributes.textDecorationStyle = TextDecorationStyle::Dotted; } else if (props.textDecorationStyle == T3MarkdownTextRunTextDecorationStyle::Dashed) { textAttributes.textDecorationStyle = TextDecorationStyle::Dashed; } else if (props.textDecorationStyle == T3MarkdownTextRunTextDecorationStyle::Double) { textAttributes.textDecorationStyle = TextDecorationStyle::Double; } if (props.textAlign == T3MarkdownTextRunTextAlign::Left) { textAttributes.alignment = TextAlignment::Left; } else if (props.textAlign == T3MarkdownTextRunTextAlign::Right) { textAttributes.alignment = TextAlignment::Right; } else if (props.textAlign == T3MarkdownTextRunTextAlign::Center) { textAttributes.alignment = TextAlignment::Center; } else if (props.textAlign == T3MarkdownTextRunTextAlign::Justify) { textAttributes.alignment = TextAlignment::Justified; } else if (props.textAlign == T3MarkdownTextRunTextAlign::Auto) { textAttributes.alignment = TextAlignment::Natural; } textAttributes.backgroundColor = props.backgroundColor; fragment.string = props.text; fragment.textAttributes = textAttributes; NSString *fragmentText = [NSString stringWithUTF8String:props.text.c_str()]; const size_t fragmentLength = fragmentText.length; if (hasParagraphStyle) { paragraphStyleRanges.push_back(T3MarkdownTextParagraphStyleRange{ utf16Offset, fragmentLength, props.shadowOffset.width, props.shadowOffset.height, props.shadowRadius - ParagraphStyleEncodingOffset, }); } if (props.nativeId.rfind(FileAttachmentNativeIdPrefix, 0) == 0 && fragmentLength > 0) { attachmentRanges.push_back(T3MarkdownTextAttachmentRange{ utf16Offset, 1, props.nativeId.substr(std::char_traits::length(FileAttachmentNativeIdPrefix)), }); } else if ( props.nativeId.rfind(SkillAttachmentNativeIdPrefix, 0) == 0 && fragmentLength > 0) { attachmentRanges.push_back(T3MarkdownTextAttachmentRange{ utf16Offset, 1, props.nativeId.substr( std::char_traits::length(SkillAttachmentNativeIdPrefix)), }); } utf16Offset += fragmentLength; baseAttributedString.appendFragment(std::move(fragment)); } } _attributedString = baseAttributedString; _paragraphStyleRanges = paragraphStyleRanges; _attachmentRanges = attachmentRanges; NSMutableAttributedString *convertedAttributedString = [RCTNSAttributedStringFromAttributedString(baseAttributedString) mutableCopy]; applyParagraphStyles(convertedAttributedString, paragraphStyleRanges); applyAttachments(convertedAttributedString, attachmentRanges); const CGFloat maximumWidth = std::isfinite(layoutConstraints.maximumSize.width) ? layoutConstraints.maximumSize.width : CGFLOAT_MAX; NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:convertedAttributedString]; NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; layoutManager.usesFontLeading = NO; NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(maximumWidth, CGFLOAT_MAX)]; textContainer.lineFragmentPadding = 0; textContainer.maximumNumberOfLines = baseProps.numberOfLines; if (baseProps.ellipsizeMode == T3MarkdownTextEllipsizeMode::Head) { textContainer.lineBreakMode = NSLineBreakByTruncatingHead; } else if (baseProps.ellipsizeMode == T3MarkdownTextEllipsizeMode::Middle) { textContainer.lineBreakMode = NSLineBreakByTruncatingMiddle; } else if (baseProps.ellipsizeMode == T3MarkdownTextEllipsizeMode::Tail) { textContainer.lineBreakMode = NSLineBreakByTruncatingTail; } else { textContainer.lineBreakMode = NSLineBreakByClipping; } [layoutManager addTextContainer:textContainer]; [textStorage addLayoutManager:layoutManager]; [layoutManager ensureLayoutForTextContainer:textContainer]; const CGRect usedRect = [layoutManager usedRectForTextContainer:textContainer]; return { std::clamp( static_cast(std::ceil(usedRect.size.width)), layoutConstraints.minimumSize.width, layoutConstraints.maximumSize.width), std::clamp( static_cast(std::ceil(usedRect.size.height)), layoutConstraints.minimumSize.height, layoutConstraints.maximumSize.height), }; } void T3MarkdownTextShadowNode::layout(LayoutContext layoutContext) { ensureUnsealed(); setStateData(T3MarkdownTextStateReal{ _attributedString, _paragraphStyleRanges, _attachmentRanges, }); } }