Skip to content

Commit

Permalink
Merge branch 'main' into @tomekzaw/android-optimize-markdown-formatter
Browse files Browse the repository at this point in the history
  • Loading branch information
tomekzaw committed Dec 9, 2024
2 parents 2b11e98 + c35696d commit ab28396
Show file tree
Hide file tree
Showing 13 changed files with 227 additions and 176 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public MarkdownFormatter(@NonNull AssetManager assetManager) {
mAssetManager = assetManager;
}

public void format(SpannableStringBuilder ssb, List<MarkdownRange> markdownRanges, @NonNull MarkdownStyle markdownStyle) {
public void format(@NonNull SpannableStringBuilder ssb, @NonNull List<MarkdownRange> markdownRanges, @NonNull MarkdownStyle markdownStyle) {
try {
Systrace.beginSection(0, "format");
Objects.requireNonNull(markdownStyle, "mMarkdownStyle is null");
Expand All @@ -45,7 +45,7 @@ public void format(SpannableStringBuilder ssb, List<MarkdownRange> markdownRange
}
}

private void removeSpans(SpannableStringBuilder ssb) {
private void removeSpans(@NonNull SpannableStringBuilder ssb) {
try {
Systrace.beginSection(0, "removeSpans");
// We shouldn't use `removeSpans()` because it also removes SpellcheckSpan, SuggestionSpan etc.
Expand All @@ -58,7 +58,7 @@ private void removeSpans(SpannableStringBuilder ssb) {
}
}

private void applyRanges(SpannableStringBuilder ssb, List<MarkdownRange> markdownRanges, @NonNull MarkdownStyle markdownStyle) {
private void applyRanges(@NonNull SpannableStringBuilder ssb, @NonNull List<MarkdownRange> markdownRanges, @NonNull MarkdownStyle markdownStyle) {
try {
Systrace.beginSection(0, "applyRanges");
for (MarkdownRange markdownRange : markdownRanges) {
Expand All @@ -69,10 +69,10 @@ private void applyRanges(SpannableStringBuilder ssb, List<MarkdownRange> markdow
}
}

private void applyRange(SpannableStringBuilder ssb, MarkdownRange markdownRange, MarkdownStyle markdownStyle) {
private void applyRange(@NonNull SpannableStringBuilder ssb, @NonNull MarkdownRange markdownRange, @NonNull MarkdownStyle markdownStyle) {
String type = markdownRange.getType();
int start = markdownRange.getStart();
int end = start + markdownRange.getLength();
int end = markdownRange.getEnd();
switch (type) {
case "bold":
setSpan(ssb, new MarkdownBoldSpan(), start, end);
Expand Down Expand Up @@ -140,7 +140,7 @@ private void applyRange(SpannableStringBuilder ssb, MarkdownRange markdownRange,
}
}

private void setSpan(SpannableStringBuilder ssb, MarkdownSpan span, int start, int end) {
private void setSpan(@NonNull SpannableStringBuilder ssb, @NonNull MarkdownSpan span, int start, int end) {
ssb.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ public MarkdownParser(@NonNull ReactContext reactContext) {
mReactContext = reactContext;
}

private native String nativeParse(String text, int parserId);
private native String nativeParse(@NonNull String text, int parserId);

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package com.expensify.livemarkdown;

import androidx.annotation.NonNull;

public class MarkdownRange {
private final String mType;
private final @NonNull String mType;
private final int mStart;
private final int mEnd;
private final int mLength;
private final int mDepth;

public MarkdownRange(String type, int start, int length, int depth) {
public MarkdownRange(@NonNull String type, int start, int length, int depth) {
mType = type;
mStart = start;
mEnd = start + length;
mLength = length;
mDepth = depth;
}
Expand All @@ -21,6 +25,10 @@ public int getStart() {
return mStart;
}

public int getEnd() {
return mEnd;
}

public int getLength() {
return mLength;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public class MarkdownStyle {

private final float mBlockquotePaddingLeft;

@NonNull
private final String mCodeFontFamily;

private final float mCodeFontSize;
Expand All @@ -42,6 +43,7 @@ public class MarkdownStyle {
@ColorInt
private final int mCodeBackgroundColor;

@NonNull
private final String mPreFontFamily;

private final float mPreFontSize;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import androidx.annotation.NonNull;

public class MarkdownTextWatcher implements TextWatcher {
private final MarkdownUtils mMarkdownUtils;
private final @NonNull MarkdownUtils mMarkdownUtils;

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

@Override
public void afterTextChanged(Editable editable) {
if (editable instanceof SpannableStringBuilder) {
mMarkdownUtils.applyMarkdownFormatting((SpannableStringBuilder) editable);
if (editable instanceof SpannableStringBuilder ssb) {
mMarkdownUtils.applyMarkdownFormatting(ssb);
}
}
}
18 changes: 18 additions & 0 deletions apple/MarkdownFormatter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#import <Foundation/Foundation.h>
#import <RNLiveMarkdown/MarkdownRange.h>
#import <RNLiveMarkdown/RCTMarkdownStyle.h>

NS_ASSUME_NONNULL_BEGIN

const NSAttributedStringKey RCTLiveMarkdownBlockquoteDepthAttributeName = @"RCTLiveMarkdownBlockquoteDepth";

@interface MarkdownFormatter : NSObject

- (nonnull NSAttributedString *)format:(nonnull NSString *)text
withAttributes:(nullable NSDictionary<NSAttributedStringKey, id>*)attributes
withMarkdownRanges:(nonnull NSArray<MarkdownRange *> *)markdownRanges
withMarkdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle;

NS_ASSUME_NONNULL_END

@end
158 changes: 158 additions & 0 deletions apple/MarkdownFormatter.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#import "MarkdownFormatter.h"
#import <React/RCTFont.h>

@implementation MarkdownFormatter

- (nonnull NSAttributedString *)format:(nonnull NSString *)text
withAttributes:(nullable NSDictionary<NSAttributedStringKey, id> *)attributes
withMarkdownRanges:(nonnull NSArray<MarkdownRange *> *)markdownRanges
withMarkdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle
{
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:text attributes:attributes];

[attributedString beginEditing];

// If the attributed string ends with underlined text, blurring the single-line input imprints the underline style across the whole string.
// It looks like a bug in iOS, as there is no underline style to be found in the attributed string, especially after formatting.
// This is a workaround that applies the NSUnderlineStyleNone to the string before iterating over ranges which resolves this problem.
[attributedString addAttribute:NSUnderlineStyleAttributeName
value:[NSNumber numberWithInteger:NSUnderlineStyleNone]
range:NSMakeRange(0, attributedString.length)];

for (MarkdownRange *markdownRange in markdownRanges) {
[self applyRangeToAttributedString:attributedString
type:std::string([markdownRange.type UTF8String])
range:markdownRange.range
depth:markdownRange.depth
markdownStyle:markdownStyle];
}

RCTApplyBaselineOffset(attributedString);

[attributedString endEditing];

return attributedString;
}

- (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedString
type:(const std::string)type
range:(const NSRange)range
depth:(const int)depth
markdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle {
if (type == "bold" || type == "italic" || type == "code" || type == "pre" || type == "h1" || type == "emoji") {
UIFont *font = [attributedString attribute:NSFontAttributeName atIndex:range.location effectiveRange:NULL];
if (type == "bold") {
font = [RCTFont updateFont:font withWeight:@"bold"];
} else if (type == "italic") {
font = [RCTFont updateFont:font withStyle:@"italic"];
} else if (type == "code") {
font = [RCTFont updateFont:font withFamily:markdownStyle.codeFontFamily
size:[NSNumber numberWithFloat:markdownStyle.codeFontSize]
weight:nil
style:nil
variant:nil
scaleMultiplier:0];
} else if (type == "pre") {
font = [RCTFont updateFont:font withFamily:markdownStyle.preFontFamily
size:[NSNumber numberWithFloat:markdownStyle.preFontSize]
weight:nil
style:nil
variant:nil
scaleMultiplier:0];
} else if (type == "h1") {
font = [RCTFont updateFont:font withFamily:nil
size:[NSNumber numberWithFloat:markdownStyle.h1FontSize]
weight:@"bold"
style:nil
variant:nil
scaleMultiplier:0];
} else if (type == "emoji") {
font = [RCTFont updateFont:font withFamily:nil
size:[NSNumber numberWithFloat:markdownStyle.emojiFontSize]
weight:nil
style:nil
variant:nil
scaleMultiplier:0];
}
[attributedString addAttribute:NSFontAttributeName value:font range:range];
}

if (type == "syntax") {
[attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.syntaxColor range:range];
} else if (type == "strikethrough") {
[attributedString addAttribute:NSStrikethroughStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range];
} else if (type == "code") {
[attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.codeColor range:range];
[attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.codeBackgroundColor range:range];
} else if (type == "mention-here") {
[attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionHereColor range:range];
[attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionHereBackgroundColor range:range];
} else if (type == "mention-user") {
// TODO: change mention color when it mentions current user
[attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionUserColor range:range];
[attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionUserBackgroundColor range:range];
} else if (type == "mention-report") {
[attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionReportColor range:range];
[attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionReportBackgroundColor range:range];
} else if (type == "link") {
[attributedString addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range];
[attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.linkColor range:range];
} else if (type == "blockquote") {
CGFloat indent = (markdownStyle.blockquoteMarginLeft + markdownStyle.blockquoteBorderWidth + markdownStyle.blockquotePaddingLeft) * depth;
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.firstLineHeadIndent = indent;
paragraphStyle.headIndent = indent;
[attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range];
[attributedString addAttribute:RCTLiveMarkdownBlockquoteDepthAttributeName value:@(depth) range:range];
} else if (type == "pre") {
[attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.preColor range:range];
NSRange rangeForBackground = [[attributedString string] 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
}
}

static void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText)
{
__block CGFloat maximumLineHeight = 0;

[attributedText enumerateAttribute:NSParagraphStyleAttributeName
inRange:NSMakeRange(0, attributedText.length)
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
usingBlock:^(NSParagraphStyle *paragraphStyle, __unused NSRange range, __unused BOOL *stop) {
if (!paragraphStyle) {
return;
}

maximumLineHeight = MAX(paragraphStyle.maximumLineHeight, maximumLineHeight);
}];

if (maximumLineHeight == 0) {
// `lineHeight` was not specified, nothing to do.
return;
}

__block CGFloat maximumFontLineHeight = 0;

[attributedText enumerateAttribute:NSFontAttributeName
inRange:NSMakeRange(0, attributedText.length)
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
usingBlock:^(UIFont *font, NSRange range, __unused BOOL *stop) {
if (!font) {
return;
}

maximumFontLineHeight = MAX(font.lineHeight, maximumFontLineHeight);
}];

if (maximumLineHeight < maximumFontLineHeight) {
return;
}

CGFloat baseLineOffset = (maximumLineHeight - maximumFontLineHeight) / 2.0;
[attributedText addAttribute:NSBaselineOffsetAttributeName
value:@(baseLineOffset)
range:NSMakeRange(0, attributedText.length)];
}

@end
1 change: 1 addition & 0 deletions apple/MarkdownLayoutManager.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#import <UIKit/UIKit.h>
#import <RNLiveMarkdown/RCTMarkdownUtils.h>
#import <RNLiveMarkdown/MarkdownFormatter.h>

NS_ASSUME_NONNULL_BEGIN

Expand Down
2 changes: 0 additions & 2 deletions apple/RCTMarkdownUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

NS_ASSUME_NONNULL_BEGIN

const NSAttributedStringKey RCTLiveMarkdownBlockquoteDepthAttributeName = @"RCTLiveMarkdownBlockquoteDepth";

@interface RCTMarkdownUtils : NSObject

@property (nonatomic) RCTMarkdownStyle *markdownStyle;
Expand Down
Loading

0 comments on commit ab28396

Please sign in to comment.