Skip to content

Commit

Permalink
Implement code and pre blocks support on iOS
Browse files Browse the repository at this point in the history
  • Loading branch information
maksg committed Apr 23, 2024
1 parent 60d75ff commit 76bed9d
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 35 deletions.
179 changes: 149 additions & 30 deletions ios/MarkdownLayoutManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,158 @@

@implementation MarkdownLayoutManager

- (BOOL)isRange:(NSRange)smallerRange inRange:(NSRange)largerRange {
NSUInteger start = smallerRange.location;
NSUInteger end = start + smallerRange.length;
NSUInteger location = largerRange.location;
return location >= start && location < end;
}

- (CGRect)rectByAddingPadding:(CGFloat)padding toRect:(CGRect)rect {
rect.origin.x -= padding;
rect.origin.y -= padding;
rect.size.width += padding * 2;
rect.size.height += padding * 2;
return rect;
}

- (void)drawBackgroundForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin {
[super drawBackgroundForGlyphRange:glyphsToShow atPoint:origin];

[self enumerateLineFragmentsForGlyphRange:glyphsToShow usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer * _Nonnull textContainer, NSRange glyphRange, BOOL * _Nonnull stop) {
__block BOOL isBlockquote = NO;
__block int currentDepth = 0;
RCTMarkdownUtils *markdownUtils = [self valueForKey:@"markdownUtils"];
[markdownUtils.blockquoteRangesAndLevels enumerateObjectsUsingBlock:^(NSDictionary *item, NSUInteger idx, BOOL * _Nonnull stop) {
NSRange range = [[item valueForKey:@"range"] rangeValue];
currentDepth = [[item valueForKey:@"depth"] unsignedIntegerValue];
NSUInteger start = range.location;
NSUInteger end = start + range.length;
NSUInteger location = glyphRange.location;
if (location >= start && location < end) {
isBlockquote = YES;
*stop = YES;
}
[super drawBackgroundForGlyphRange:glyphsToShow atPoint:origin];

RCTMarkdownStyle *style = [_markdownUtils markdownStyle];
[self drawBlockquotesForRanges:[_markdownUtils blockquoteRangesAndLevels] andGlyphRange:glyphsToShow atPoint:origin withColor:[style blockquoteBorderColor] width:[style blockquoteBorderWidth] margin:[style blockquoteMarginLeft] andPadding:[style blockquotePaddingLeft]];
[self drawPreBackgroundForRanges:[_markdownUtils preRanges] atPoint:origin withColor:[style preBackgroundColor] borderColor:[style preBorderColor] borderWidth:[style preBorderWidth] borderRadius:[style preBorderRadius] andPadding:[style prePadding]];
[self drawCodeBackgroundForRanges:[_markdownUtils codeRanges] atPoint:origin withColor:[style codeBackgroundColor] borderColor:[style codeBorderColor] borderWidth:[style codeBorderWidth] borderRadius:[style codeBorderRadius] andPadding:[style codePadding]];
}

- (void)drawBlockquotesForRanges:(NSArray<NSDictionary*>*)ranges andGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin withColor:(UIColor*)color width:(CGFloat)width margin:(CGFloat)margin andPadding:(CGFloat)padding {
[self enumerateLineFragmentsForGlyphRange:glyphsToShow usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer * _Nonnull textContainer, NSRange glyphRange, BOOL * _Nonnull stop) {
__block BOOL isBlockquote = NO;
__block int currentDepth = 0;

[ranges enumerateObjectsUsingBlock:^(NSDictionary *item, NSUInteger idx, BOOL * _Nonnull stop) {
NSRange range = [[item valueForKey:@"range"] rangeValue];
currentDepth = [[item valueForKey:@"depth"] unsignedIntegerValue];
if ([self isRange:range inRange:glyphRange]) {
isBlockquote = YES;
*stop = YES;
}
}];
if (isBlockquote) {
CGFloat paddingLeft = origin.x;
CGFloat paddingTop = origin.y;
CGFloat y = paddingTop + rect.origin.y;
CGFloat height = rect.size.height;
CGFloat shift = margin + width + padding;
for (int level = 0; level < currentDepth; level++) {
CGFloat x = paddingLeft + (level * shift) + margin;
CGRect lineRect = CGRectMake(x, y, width, height);
[color setFill];
UIRectFill(lineRect);
}
}
}];
}

- (void)drawPreBackgroundForRanges:(NSArray<NSValue*>*)ranges atPoint:(CGPoint)origin withColor:(UIColor*)backgroundColor borderColor:(UIColor*)borderColor borderWidth:(CGFloat)borderWidth borderRadius:(CGFloat)borderRadius andPadding:(CGFloat)padding {
__block CGRect preRect = CGRectNull;
[ranges enumerateObjectsUsingBlock:^(NSValue *item, NSUInteger idx, BOOL * _Nonnull stop) {
NSRange range = [item rangeValue];
// We don't want the trailing ``` to be a part of the block so we need to reduce range by 1.
// This also breaks one character blocks so we need to check if range is larger.
if (range.length > 1) {
range.location += 1;
range.length -= 1;
}

[self enumerateLineFragmentsForGlyphRange:range usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer * _Nonnull textContainer, NSRange glyphRange, BOOL * _Nonnull stop) {
if (CGRectIsNull(preRect)) {
preRect = usedRect;
CGFloat paddingLeft = origin.x;
preRect.origin.x += paddingLeft;
CGFloat paddingTop = origin.y;
preRect.origin.y += paddingTop;
} else {
CGFloat usedWidth = usedRect.size.width;
if (usedWidth > preRect.size.width) {
preRect.size.width = usedWidth;
}
preRect.size.height += usedRect.size.height;
}
}];

if (!CGRectIsNull(preRect)) {
preRect = [self rectByAddingPadding:padding toRect:preRect];
[self drawBackgroundWithColor:backgroundColor borderColor:borderColor borderWidth:borderWidth andBorderRadius:borderRadius forRect:preRect isLeftOpen:NO isRightOpen:NO];
preRect = CGRectNull;
}
}];
}

- (void)drawCodeBackgroundForRanges:(NSArray<NSValue*>*)ranges atPoint:(CGPoint)origin withColor:(UIColor*)backgroundColor borderColor:(UIColor*)borderColor borderWidth:(CGFloat)borderWidth borderRadius:(CGFloat)borderRadius andPadding:(CGFloat)padding {
[ranges enumerateObjectsUsingBlock:^(NSValue *item, NSUInteger idx, BOOL * _Nonnull stop) {
NSRange range = [item rangeValue];
[self enumerateLineFragmentsForGlyphRange:range usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer * _Nonnull textContainer, NSRange glyphRange, BOOL * _Nonnull stop) {
BOOL isLeftSideOpen = YES;
BOOL isRightSideOpen = YES;

NSRange adjustedRange = glyphRange;
if (range.location > adjustedRange.location) {
adjustedRange.length -= range.location - adjustedRange.location;
adjustedRange.location = range.location;
isLeftSideOpen = NO;
}

NSUInteger rangeEndLocation = range.location + range.length;
NSUInteger adjustedRangeEndLocation = adjustedRange.location + adjustedRange.length;
if (rangeEndLocation < adjustedRangeEndLocation) {
adjustedRange.length -= adjustedRangeEndLocation - rangeEndLocation;
isRightSideOpen = NO;
}

CGRect codeRect = [self boundingRectForGlyphRange:adjustedRange inTextContainer:textContainer];
CGFloat paddingLeft = origin.x;
codeRect.origin.x += paddingLeft;
CGFloat paddingTop = origin.y;
codeRect.origin.y += paddingTop;
codeRect = [self rectByAddingPadding:padding toRect:codeRect];
[self drawBackgroundWithColor:backgroundColor borderColor:borderColor borderWidth:borderWidth andBorderRadius:borderRadius forRect:codeRect isLeftOpen:isLeftSideOpen isRightOpen:isRightSideOpen];
}];
}];
if (isBlockquote) {
CGFloat paddingLeft = origin.x;
CGFloat paddingTop = origin.y;
CGFloat y = paddingTop + rect.origin.y;
CGFloat width = markdownUtils.markdownStyle.blockquoteBorderWidth;
CGFloat height = rect.size.height;
CGFloat shift = markdownUtils.markdownStyle.blockquoteMarginLeft + markdownUtils.markdownStyle.blockquoteBorderWidth + markdownUtils.markdownStyle.blockquotePaddingLeft;
for (int level = 0; level < currentDepth; level++) {
CGFloat x = paddingLeft + (level * shift) + markdownUtils.markdownStyle.blockquoteMarginLeft;
CGRect lineRect = CGRectMake(x, y, width, height);
[markdownUtils.markdownStyle.blockquoteBorderColor setFill];
UIRectFill(lineRect);
}
}

- (void)drawBackgroundWithColor:(UIColor*)backgroundColor borderColor:(UIColor*)borderColor borderWidth:(CGFloat)borderWidth andBorderRadius:(CGFloat)radius forRect:(CGRect)rect isLeftOpen:(BOOL)isLeftOpen isRightOpen:(BOOL)isRightOpen {
UIRectCorner corners = 0;
if (!isLeftOpen) {
corners |= UIRectCornerTopLeft | UIRectCornerBottomLeft;
}
}];
if (!isRightOpen) {
corners |= UIRectCornerTopRight | UIRectCornerBottomRight;
}
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:CGSizeMake(radius, radius)];

[backgroundColor setFill];
[path fill];
[borderColor setStroke];
[path setLineWidth:borderWidth];
[path stroke];

if (isLeftOpen) {
[self openSideForRect:rect withBorderWidth:borderWidth isLeft:YES];
}
if (isRightOpen) {
[self openSideForRect:rect withBorderWidth:borderWidth isLeft:NO];
}
}

- (void)openSideForRect:(CGRect)rect withBorderWidth:(CGFloat)borderWidth isLeft:(BOOL)isLeft {
UIBezierPath *path = [[UIBezierPath alloc] init];
CGFloat x = isLeft ? CGRectGetMinX(rect) : CGRectGetMaxX(rect);
[path moveToPoint:CGPointMake(x, CGRectGetMinY(rect) - borderWidth)];
[path addLineToPoint:CGPointMake(x, CGRectGetMaxY(rect) + borderWidth)];
[[UIColor clearColor] setStroke];
[path setLineWidth:borderWidth + 1];
[path strokeWithBlendMode:kCGBlendModeClear alpha:1.0];
}

@end
8 changes: 8 additions & 0 deletions ios/RCTMarkdownStyle.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,18 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic) CGFloat codeFontSize;
@property (nonatomic) UIColor *codeColor;
@property (nonatomic) UIColor *codeBackgroundColor;
@property (nonatomic) UIColor *codeBorderColor;
@property (nonatomic) CGFloat codeBorderWidth;
@property (nonatomic) CGFloat codeBorderRadius;
@property (nonatomic) CGFloat codePadding;
@property (nonatomic) NSString *preFontFamily;
@property (nonatomic) CGFloat preFontSize;
@property (nonatomic) UIColor *preColor;
@property (nonatomic) UIColor *preBackgroundColor;
@property (nonatomic) UIColor *preBorderColor;
@property (nonatomic) CGFloat preBorderWidth;
@property (nonatomic) CGFloat preBorderRadius;
@property (nonatomic) CGFloat prePadding;
@property (nonatomic) UIColor *mentionHereColor;
@property (nonatomic) UIColor *mentionHereBackgroundColor;
@property (nonatomic) UIColor *mentionUserColor;
Expand Down
16 changes: 16 additions & 0 deletions ios/RCTMarkdownStyle.mm
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,19 @@ - (instancetype)initWithStruct:(const facebook::react::MarkdownTextInputDecorato
_codeFontSize = style.code.fontSize;
_codeColor = RCTUIColorFromSharedColor(style.code.color);
_codeBackgroundColor = RCTUIColorFromSharedColor(style.code.backgroundColor);
_codeBorderColor = RCTUIColorFromSharedColor(style.code.borderColor);
_codeBorderWidth = style.code.borderWidth;
_codeBorderRadius = style.code.borderRadius;
_codePadding = style.code.padding;

_preFontFamily = RCTNSStringFromString(style.pre.fontFamily);
_preFontSize = style.pre.fontSize;
_preColor = RCTUIColorFromSharedColor(style.pre.color);
_preBackgroundColor = RCTUIColorFromSharedColor(style.pre.backgroundColor);
_preBorderColor = RCTUIColorFromSharedColor(style.pre.borderColor);
_preBorderWidth = style.pre.borderWidth;
_preBorderRadius = style.pre.borderRadius;
_prePadding = style.pre.padding;

_mentionHereColor = RCTUIColorFromSharedColor(style.mentionHere.color);
_mentionHereBackgroundColor = RCTUIColorFromSharedColor(style.mentionHere.backgroundColor);
Expand Down Expand Up @@ -68,11 +76,19 @@ - (instancetype)initWithDictionary:(NSDictionary *)json
_codeFontSize = [RCTConvert CGFloat:json[@"code"][@"fontSize"]];
_codeColor = [RCTConvert UIColor:json[@"code"][@"color"]];
_codeBackgroundColor = [RCTConvert UIColor:json[@"code"][@"backgroundColor"]];
_codeBorderColor = [RCTConvert UIColor:json[@"code"][@"borderColor"]];
_codeBorderWidth = [RCTConvert CGFloat:json[@"code"][@"borderWidth"]];
_codeBorderRadius = [RCTConvert CGFloat:json[@"code"][@"borderRadius"]];
_codePadding = [RCTConvert CGFloat:json[@"code"][@"padding"]];

_preFontFamily = [RCTConvert NSString:json[@"pre"][@"fontFamily"]];
_preFontSize = [RCTConvert CGFloat:json[@"pre"][@"fontSize"]];
_preColor = [RCTConvert UIColor:json[@"pre"][@"color"]];
_preBackgroundColor = [RCTConvert UIColor:json[@"pre"][@"backgroundColor"]];
_preBorderColor = [RCTConvert UIColor:json[@"pre"][@"borderColor"]];
_preBorderWidth = [RCTConvert CGFloat:json[@"pre"][@"borderWidth"]];
_preBorderRadius = [RCTConvert CGFloat:json[@"pre"][@"borderRadius"]];
_prePadding = [RCTConvert CGFloat:json[@"pre"][@"padding"]];

_mentionHereColor = [RCTConvert UIColor:json[@"mentionHere"][@"color"]];
_mentionHereBackgroundColor = [RCTConvert UIColor:json[@"mentionHere"][@"backgroundColor"]];
Expand Down
2 changes: 2 additions & 0 deletions ios/RCTMarkdownUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ NS_ASSUME_NONNULL_BEGIN

@property (nonatomic) RCTMarkdownStyle *markdownStyle;
@property (nonatomic) NSMutableArray<NSDictionary *> *blockquoteRangesAndLevels;
@property (nonatomic) NSMutableArray<NSValue *> *codeRanges;
@property (nonatomic) NSMutableArray<NSValue *> *preRanges;

- (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withAttributes:(nullable NSDictionary<NSAttributedStringKey, id>*)attributes;

Expand Down
14 changes: 9 additions & 5 deletions ios/RCTMarkdownUtils.mm
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withA
[attributedString addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleNone] range:NSMakeRange(0, attributedString.length)];

_blockquoteRangesAndLevels = [NSMutableArray new];
_codeRanges = [NSMutableArray new];
_preRanges = [NSMutableArray new];

[ranges enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSDictionary *item = obj;
Expand Down Expand Up @@ -100,7 +102,7 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withA
[attributedString addAttribute:NSStrikethroughStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range];
} else if ([type isEqualToString:@"code"]) {
[attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.codeColor range:range];
[attributedString addAttribute:NSBackgroundColorAttributeName value:_markdownStyle.codeBackgroundColor range:range];
[_codeRanges addObject:[NSValue valueWithRange:range]];
} else if ([type isEqualToString:@"mention-here"]) {
[attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.mentionHereColor range:range];
[attributedString addAttribute:NSBackgroundColorAttributeName value:_markdownStyle.mentionHereBackgroundColor range:range];
Expand All @@ -122,10 +124,13 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withA
@"depth": @(depth)
}];
} else if ([type isEqualToString:@"pre"]) {
CGFloat indent = _markdownStyle.prePadding;
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.firstLineHeadIndent = indent;
paragraphStyle.headIndent = indent;
[attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range];
[attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.preColor range:range];
NSRange rangeForBackground = [inputString characterAtIndex:range.location] == '\n' ? NSMakeRange(range.location + 1, range.length - 1) : range;
[attributedString addAttribute:NSBackgroundColorAttributeName value:_markdownStyle.preBackgroundColor range:rangeForBackground];
// TODO: pass background color and ranges to layout manager
[_preRanges addObject:[NSValue valueWithRange:range]];
} else if ([type isEqualToString:@"h1"]) {
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
NSRange rangeWithHashAndSpace = NSMakeRange(range.location - 2, range.length + 2); // we also need to include prepending "# "
Expand All @@ -141,7 +146,6 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withA
_prevMarkdownStyle = _markdownStyle;

return attributedString;

}
}

Expand Down
8 changes: 8 additions & 0 deletions src/MarkdownTextInputDecoratorViewNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,20 @@ interface MarkdownStyle {
fontSize: Float;
color: ColorValue;
backgroundColor: ColorValue;
borderColor: ColorValue;
borderWidth: Float;
borderRadius: Float;
padding: Float;
};
pre: {
fontFamily: string;
fontSize: Float;
color: ColorValue;
backgroundColor: ColorValue;
borderColor: ColorValue;
borderWidth: Float;
borderRadius: Float;
padding: Float;
};
mentionHere: {
color: ColorValue;
Expand Down
8 changes: 8 additions & 0 deletions src/styleUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,20 @@ function makeDefaultMarkdownStyle(): MarkdownStyle {
fontSize: 20,
color: 'black',
backgroundColor: 'lightgray',
borderColor: 'gray',
borderWidth: 1,
borderRadius: 4,
padding: 0,
},
pre: {
fontFamily: FONT_FAMILY_MONOSPACE,
fontSize: 20,
color: 'black',
backgroundColor: 'lightgray',
borderColor: 'gray',
borderWidth: 1,
borderRadius: 4,
padding: 2,
},
mentionHere: {
color: 'green',
Expand Down

0 comments on commit 76bed9d

Please sign in to comment.