Skip to content

Commit aa2e474

Browse files
committed
Merge branch 'main' into @tomekzaw/android-improve-remove-spans
2 parents 42c7588 + c35696d commit aa2e474

13 files changed

+227
-176
lines changed

android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public MarkdownFormatter(@NonNull AssetManager assetManager) {
2222
mAssetManager = assetManager;
2323
}
2424

25-
public void format(SpannableStringBuilder ssb, List<MarkdownRange> markdownRanges, @NonNull MarkdownStyle markdownStyle) {
25+
public void format(@NonNull SpannableStringBuilder ssb, @NonNull List<MarkdownRange> markdownRanges, @NonNull MarkdownStyle markdownStyle) {
2626
try {
2727
Systrace.beginSection(0, "format");
2828
Objects.requireNonNull(markdownStyle, "mMarkdownStyle is null");
@@ -33,7 +33,7 @@ public void format(SpannableStringBuilder ssb, List<MarkdownRange> markdownRange
3333
}
3434
}
3535

36-
private void removeSpans(SpannableStringBuilder ssb) {
36+
private void removeSpans(@NonNull SpannableStringBuilder ssb) {
3737
try {
3838
Systrace.beginSection(0, "removeSpans");
3939
// We shouldn't use `ssb.removeSpans()` because it also removes SpellcheckSpan, SuggestionSpan etc.
@@ -46,7 +46,7 @@ private void removeSpans(SpannableStringBuilder ssb) {
4646
}
4747
}
4848

49-
private void applyRanges(SpannableStringBuilder ssb, List<MarkdownRange> markdownRanges, @NonNull MarkdownStyle markdownStyle) {
49+
private void applyRanges(@NonNull SpannableStringBuilder ssb, @NonNull List<MarkdownRange> markdownRanges, @NonNull MarkdownStyle markdownStyle) {
5050
try {
5151
Systrace.beginSection(0, "applyRanges");
5252
for (MarkdownRange markdownRange : markdownRanges) {
@@ -57,10 +57,10 @@ private void applyRanges(SpannableStringBuilder ssb, List<MarkdownRange> markdow
5757
}
5858
}
5959

60-
private void applyRange(SpannableStringBuilder ssb, MarkdownRange markdownRange, MarkdownStyle markdownStyle) {
60+
private void applyRange(@NonNull SpannableStringBuilder ssb, @NonNull MarkdownRange markdownRange, @NonNull MarkdownStyle markdownStyle) {
6161
String type = markdownRange.getType();
6262
int start = markdownRange.getStart();
63-
int end = start + markdownRange.getLength();
63+
int end = markdownRange.getEnd();
6464
switch (type) {
6565
case "bold":
6666
setSpan(ssb, new MarkdownBoldSpan(), start, end);
@@ -128,7 +128,7 @@ private void applyRange(SpannableStringBuilder ssb, MarkdownRange markdownRange,
128128
}
129129
}
130130

131-
private void setSpan(SpannableStringBuilder ssb, MarkdownSpan span, int start, int end) {
131+
private void setSpan(@NonNull SpannableStringBuilder ssb, @NonNull MarkdownSpan span, int start, int end) {
132132
ssb.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
133133
mMarkdownSpans.add(span);
134134
}

android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ public MarkdownParser(@NonNull ReactContext reactContext) {
2929
mReactContext = reactContext;
3030
}
3131

32-
private native String nativeParse(String text, int parserId);
32+
private native String nativeParse(@NonNull String text, int parserId);
3333

34-
public synchronized List<MarkdownRange> parse(String text, int parserId) {
34+
public synchronized List<MarkdownRange> parse(@NonNull String text, int parserId) {
3535
try {
3636
Systrace.beginSection(0, "parse");
3737

android/src/main/java/com/expensify/livemarkdown/MarkdownRange.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
package com.expensify.livemarkdown;
22

3+
import androidx.annotation.NonNull;
4+
35
public class MarkdownRange {
4-
private final String mType;
6+
private final @NonNull String mType;
57
private final int mStart;
8+
private final int mEnd;
69
private final int mLength;
710
private final int mDepth;
811

9-
public MarkdownRange(String type, int start, int length, int depth) {
12+
public MarkdownRange(@NonNull String type, int start, int length, int depth) {
1013
mType = type;
1114
mStart = start;
15+
mEnd = start + length;
1216
mLength = length;
1317
mDepth = depth;
1418
}
@@ -21,6 +25,10 @@ public int getStart() {
2125
return mStart;
2226
}
2327

28+
public int getEnd() {
29+
return mEnd;
30+
}
31+
2432
public int getLength() {
2533
return mLength;
2634
}

android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public class MarkdownStyle {
3232

3333
private final float mBlockquotePaddingLeft;
3434

35+
@NonNull
3536
private final String mCodeFontFamily;
3637

3738
private final float mCodeFontSize;
@@ -42,6 +43,7 @@ public class MarkdownStyle {
4243
@ColorInt
4344
private final int mCodeBackgroundColor;
4445

46+
@NonNull
4547
private final String mPreFontFamily;
4648

4749
private final float mPreFontSize;

android/src/main/java/com/expensify/livemarkdown/MarkdownTextWatcher.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import androidx.annotation.NonNull;
88

99
public class MarkdownTextWatcher implements TextWatcher {
10-
private final MarkdownUtils mMarkdownUtils;
10+
private final @NonNull MarkdownUtils mMarkdownUtils;
1111

1212
public MarkdownTextWatcher(@NonNull MarkdownUtils markdownUtils) {
1313
mMarkdownUtils = markdownUtils;
@@ -25,8 +25,8 @@ public void onTextChanged(CharSequence s, int start, int before, int count) {
2525

2626
@Override
2727
public void afterTextChanged(Editable editable) {
28-
if (editable instanceof SpannableStringBuilder) {
29-
mMarkdownUtils.applyMarkdownFormatting((SpannableStringBuilder) editable);
28+
if (editable instanceof SpannableStringBuilder ssb) {
29+
mMarkdownUtils.applyMarkdownFormatting(ssb);
3030
}
3131
}
3232
}

apple/MarkdownFormatter.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#import <Foundation/Foundation.h>
2+
#import <RNLiveMarkdown/MarkdownRange.h>
3+
#import <RNLiveMarkdown/RCTMarkdownStyle.h>
4+
5+
NS_ASSUME_NONNULL_BEGIN
6+
7+
const NSAttributedStringKey RCTLiveMarkdownBlockquoteDepthAttributeName = @"RCTLiveMarkdownBlockquoteDepth";
8+
9+
@interface MarkdownFormatter : NSObject
10+
11+
- (nonnull NSAttributedString *)format:(nonnull NSString *)text
12+
withAttributes:(nullable NSDictionary<NSAttributedStringKey, id>*)attributes
13+
withMarkdownRanges:(nonnull NSArray<MarkdownRange *> *)markdownRanges
14+
withMarkdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle;
15+
16+
NS_ASSUME_NONNULL_END
17+
18+
@end

apple/MarkdownFormatter.mm

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#import "MarkdownFormatter.h"
2+
#import <React/RCTFont.h>
3+
4+
@implementation MarkdownFormatter
5+
6+
- (nonnull NSAttributedString *)format:(nonnull NSString *)text
7+
withAttributes:(nullable NSDictionary<NSAttributedStringKey, id> *)attributes
8+
withMarkdownRanges:(nonnull NSArray<MarkdownRange *> *)markdownRanges
9+
withMarkdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle
10+
{
11+
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:text attributes:attributes];
12+
13+
[attributedString beginEditing];
14+
15+
// If the attributed string ends with underlined text, blurring the single-line input imprints the underline style across the whole string.
16+
// It looks like a bug in iOS, as there is no underline style to be found in the attributed string, especially after formatting.
17+
// This is a workaround that applies the NSUnderlineStyleNone to the string before iterating over ranges which resolves this problem.
18+
[attributedString addAttribute:NSUnderlineStyleAttributeName
19+
value:[NSNumber numberWithInteger:NSUnderlineStyleNone]
20+
range:NSMakeRange(0, attributedString.length)];
21+
22+
for (MarkdownRange *markdownRange in markdownRanges) {
23+
[self applyRangeToAttributedString:attributedString
24+
type:std::string([markdownRange.type UTF8String])
25+
range:markdownRange.range
26+
depth:markdownRange.depth
27+
markdownStyle:markdownStyle];
28+
}
29+
30+
RCTApplyBaselineOffset(attributedString);
31+
32+
[attributedString endEditing];
33+
34+
return attributedString;
35+
}
36+
37+
- (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedString
38+
type:(const std::string)type
39+
range:(const NSRange)range
40+
depth:(const int)depth
41+
markdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle {
42+
if (type == "bold" || type == "italic" || type == "code" || type == "pre" || type == "h1" || type == "emoji") {
43+
UIFont *font = [attributedString attribute:NSFontAttributeName atIndex:range.location effectiveRange:NULL];
44+
if (type == "bold") {
45+
font = [RCTFont updateFont:font withWeight:@"bold"];
46+
} else if (type == "italic") {
47+
font = [RCTFont updateFont:font withStyle:@"italic"];
48+
} else if (type == "code") {
49+
font = [RCTFont updateFont:font withFamily:markdownStyle.codeFontFamily
50+
size:[NSNumber numberWithFloat:markdownStyle.codeFontSize]
51+
weight:nil
52+
style:nil
53+
variant:nil
54+
scaleMultiplier:0];
55+
} else if (type == "pre") {
56+
font = [RCTFont updateFont:font withFamily:markdownStyle.preFontFamily
57+
size:[NSNumber numberWithFloat:markdownStyle.preFontSize]
58+
weight:nil
59+
style:nil
60+
variant:nil
61+
scaleMultiplier:0];
62+
} else if (type == "h1") {
63+
font = [RCTFont updateFont:font withFamily:nil
64+
size:[NSNumber numberWithFloat:markdownStyle.h1FontSize]
65+
weight:@"bold"
66+
style:nil
67+
variant:nil
68+
scaleMultiplier:0];
69+
} else if (type == "emoji") {
70+
font = [RCTFont updateFont:font withFamily:nil
71+
size:[NSNumber numberWithFloat:markdownStyle.emojiFontSize]
72+
weight:nil
73+
style:nil
74+
variant:nil
75+
scaleMultiplier:0];
76+
}
77+
[attributedString addAttribute:NSFontAttributeName value:font range:range];
78+
}
79+
80+
if (type == "syntax") {
81+
[attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.syntaxColor range:range];
82+
} else if (type == "strikethrough") {
83+
[attributedString addAttribute:NSStrikethroughStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range];
84+
} else if (type == "code") {
85+
[attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.codeColor range:range];
86+
[attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.codeBackgroundColor range:range];
87+
} else if (type == "mention-here") {
88+
[attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionHereColor range:range];
89+
[attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionHereBackgroundColor range:range];
90+
} else if (type == "mention-user") {
91+
// TODO: change mention color when it mentions current user
92+
[attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionUserColor range:range];
93+
[attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionUserBackgroundColor range:range];
94+
} else if (type == "mention-report") {
95+
[attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionReportColor range:range];
96+
[attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionReportBackgroundColor range:range];
97+
} else if (type == "link") {
98+
[attributedString addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range];
99+
[attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.linkColor range:range];
100+
} else if (type == "blockquote") {
101+
CGFloat indent = (markdownStyle.blockquoteMarginLeft + markdownStyle.blockquoteBorderWidth + markdownStyle.blockquotePaddingLeft) * depth;
102+
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
103+
paragraphStyle.firstLineHeadIndent = indent;
104+
paragraphStyle.headIndent = indent;
105+
[attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range];
106+
[attributedString addAttribute:RCTLiveMarkdownBlockquoteDepthAttributeName value:@(depth) range:range];
107+
} else if (type == "pre") {
108+
[attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.preColor range:range];
109+
NSRange rangeForBackground = [[attributedString string] characterAtIndex:range.location] == '\n' ? NSMakeRange(range.location + 1, range.length - 1) : range;
110+
[attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.preBackgroundColor range:rangeForBackground];
111+
// TODO: pass background color and ranges to layout manager
112+
}
113+
}
114+
115+
static void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText)
116+
{
117+
__block CGFloat maximumLineHeight = 0;
118+
119+
[attributedText enumerateAttribute:NSParagraphStyleAttributeName
120+
inRange:NSMakeRange(0, attributedText.length)
121+
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
122+
usingBlock:^(NSParagraphStyle *paragraphStyle, __unused NSRange range, __unused BOOL *stop) {
123+
if (!paragraphStyle) {
124+
return;
125+
}
126+
127+
maximumLineHeight = MAX(paragraphStyle.maximumLineHeight, maximumLineHeight);
128+
}];
129+
130+
if (maximumLineHeight == 0) {
131+
// `lineHeight` was not specified, nothing to do.
132+
return;
133+
}
134+
135+
__block CGFloat maximumFontLineHeight = 0;
136+
137+
[attributedText enumerateAttribute:NSFontAttributeName
138+
inRange:NSMakeRange(0, attributedText.length)
139+
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
140+
usingBlock:^(UIFont *font, NSRange range, __unused BOOL *stop) {
141+
if (!font) {
142+
return;
143+
}
144+
145+
maximumFontLineHeight = MAX(font.lineHeight, maximumFontLineHeight);
146+
}];
147+
148+
if (maximumLineHeight < maximumFontLineHeight) {
149+
return;
150+
}
151+
152+
CGFloat baseLineOffset = (maximumLineHeight - maximumFontLineHeight) / 2.0;
153+
[attributedText addAttribute:NSBaselineOffsetAttributeName
154+
value:@(baseLineOffset)
155+
range:NSMakeRange(0, attributedText.length)];
156+
}
157+
158+
@end

apple/MarkdownLayoutManager.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#import <UIKit/UIKit.h>
22
#import <RNLiveMarkdown/RCTMarkdownUtils.h>
3+
#import <RNLiveMarkdown/MarkdownFormatter.h>
34

45
NS_ASSUME_NONNULL_BEGIN
56

apple/RCTMarkdownUtils.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33

44
NS_ASSUME_NONNULL_BEGIN
55

6-
const NSAttributedStringKey RCTLiveMarkdownBlockquoteDepthAttributeName = @"RCTLiveMarkdownBlockquoteDepth";
7-
86
@interface RCTMarkdownUtils : NSObject
97

108
@property (nonatomic) RCTMarkdownStyle *markdownStyle;

0 commit comments

Comments
 (0)