#include "T3MarkdownTextShadowNode.h"#include "T3MarkdownTextRunShadowNode.h"#include <react/renderer/components/view/ViewShadowNode.h>#import <react/renderer/textlayoutmanager/RCTAttributedTextUtils.h> #include <algorithm>#include <cmath> 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<T3MarkdownTextParagraphStyleRange> &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<T3MarkdownTextAttachmentRange> &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<T3MarkdownTextParagraphStyleRange>{}; auto attachmentRanges = std::vector<T3MarkdownTextAttachmentRange>{}; 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<const T3MarkdownTextRunShadowNode *>(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<char>::length(FileAttachmentNativeIdPrefix)), }); } else if ( props.nativeId.rfind(SkillAttachmentNativeIdPrefix, 0) == 0 && fragmentLength > 0) { attachmentRanges.push_back(T3MarkdownTextAttachmentRange{ utf16Offset, 1, props.nativeId.substr( std::char_traits<char>::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<Float>(std::ceil(usedRect.size.width)), layoutConstraints.minimumSize.width, layoutConstraints.maximumSize.width), std::clamp( static_cast<Float>(std::ceil(usedRect.size.height)), layoutConstraints.minimumSize.height, layoutConstraints.maximumSize.height), };} void T3MarkdownTextShadowNode::layout(LayoutContext layoutContext) { ensureUnsealed(); setStateData(T3MarkdownTextStateReal{ _attributedString, _paragraphStyleRanges, _attachmentRanges, });}}
#include "T3MarkdownTextShadowNode.h"
#include "T3MarkdownTextRunShadowNode.h"
#include <react/renderer/components/view/ViewShadowNode.h>
#import <react/renderer/textlayoutmanager/RCTAttributedTextUtils.h>
#include <algorithm>
#include <cmath>
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<T3MarkdownTextParagraphStyleRange> &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<T3MarkdownTextAttachmentRange> &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<T3MarkdownTextParagraphStyleRange>{};
auto attachmentRanges = std::vector<T3MarkdownTextAttachmentRange>{};
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<const T3MarkdownTextRunShadowNode *>(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<char>::length(FileAttachmentNativeIdPrefix)),
});
} else if (
props.nativeId.rfind(SkillAttachmentNativeIdPrefix, 0) == 0 && fragmentLength > 0) {
attachmentRanges.push_back(T3MarkdownTextAttachmentRange{
utf16Offset,
1,
props.nativeId.substr(
std::char_traits<char>::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<Float>(std::ceil(usedRect.size.width)),
layoutConstraints.minimumSize.width,
layoutConstraints.maximumSize.width),
std::clamp(
static_cast<Float>(std::ceil(usedRect.size.height)),
layoutConstraints.minimumSize.height,
layoutConstraints.maximumSize.height),
};
}
void T3MarkdownTextShadowNode::layout(LayoutContext layoutContext) {
ensureUnsealed();
setStateData(T3MarkdownTextStateReal{
_attributedString,
_paragraphStyleRanges,
_attachmentRanges,
});
}
}