Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement code and pre blocks support on web #341

Closed
wants to merge 12 commits into from
Closed
175 changes: 145 additions & 30 deletions ios/MarkdownLayoutManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,154 @@

@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];
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 @@ -71,11 +79,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
9 changes: 4 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 @@ -126,9 +128,7 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withA
}];
} else if ([type isEqualToString:@"pre"]) {
[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 @@ -146,7 +146,6 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withA
_prevMarkdownStyle = _markdownStyle;

return attributedString;

}
}

Expand Down
25 changes: 23 additions & 2 deletions src/MarkdownTextInput.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import type {CSSProperties, MutableRefObject, ReactEventHandler, FocusEventHandl
import {StyleSheet} from 'react-native';
import * as ParseUtils from './web/parserUtils';
import * as CursorUtils from './web/cursorUtils';
import * as StyleUtils from './styleUtils';
import * as BrowserUtils from './web/browserUtils';
import * as StyleUtils from './styleUtils';
import type * as MarkdownTextInputDecoratorViewNativeComponent from './MarkdownTextInputDecoratorViewNativeComponent';
import './web/MarkdownTextInput.css';
import InputHistory from './web/InputHistory';
Expand Down Expand Up @@ -299,7 +299,9 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
if (!divRef.current) {
return;
}
const newSelection = predefinedSelection || CursorUtils.getCurrentCursorPosition(divRef.current);

const isContained = CursorUtils.restrictRanges(divRef.current, (divRef.current as HTMLInputElement)?.value ?? '');
const newSelection = predefinedSelection && isContained ? predefinedSelection : CursorUtils.getCurrentCursorPosition(divRef.current);

if (newSelection && (!contentSelection.current || contentSelection.current.start !== newSelection.start || contentSelection.current.end !== newSelection.end)) {
updateRefSelectionVariables(newSelection);
Expand Down Expand Up @@ -420,6 +422,22 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
onKeyPress(event);
}

// Ensure user can't move into background spans with arrow keys
if (
(e.key === 'ArrowDown' || e.key === 'ArrowRight') &&
contentSelection.current?.end === (divRef.current as HTMLInputElement).value?.length &&
contentSelection.current?.end === contentSelection.current?.start
) {
e.preventDefault();
return;
}

// Making sure that CMD + A works on safari - due to contenteditable div containing multiple spans we need to recreate it ourselves
if (BrowserUtils.isSafari && e.key === 'a' && e.metaKey) {
e.preventDefault();
CursorUtils.setCursorPosition(divRef.current, 0, (divRef.current as HTMLInputElement).value?.length, false);
}

updateSelection(event as unknown as SyntheticEvent<HTMLDivElement, Event>);

if (
Expand Down Expand Up @@ -628,6 +646,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
onFocus={handleFocus}
onBlur={handleBlur}
onPaste={handlePaste}
onScroll={() => ParseUtils.handlePreBlockBackground(divRef.current as HTMLElement)}
placeholder={heightSafePlaceholder}
spellCheck={spellCheck}
dir={dir}
Expand All @@ -648,6 +667,8 @@ const styles = StyleSheet.create({
overflowY: 'auto',
overflowX: 'auto',
overflowWrap: 'break-word',
position: 'relative',
clipPath: 'polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)',
Skalakid marked this conversation as resolved.
Show resolved Hide resolved
},
disabledInputStyles: {
opacity: 0.75,
Expand Down
10 changes: 10 additions & 0 deletions src/MarkdownTextInputDecoratorViewNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,22 @@ interface MarkdownStyle {
fontSize: Float;
color: ColorValue;
backgroundColor: ColorValue;
borderColor: ColorValue;
borderWidth: Float;
borderRadius: Float;
borderStyle: string;
padding: Float;
};
pre: {
fontFamily: string;
fontSize: Float;
color: ColorValue;
backgroundColor: ColorValue;
borderColor: ColorValue;
borderWidth: Float;
borderRadius: Float;
borderStyle: string;
padding: Float;
};
mentionHere: {
color: ColorValue;
Expand Down
Loading
Loading