From 5cd661721d5466b4efd2b0abf631b0ba6d74eb0b Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Fri, 6 Dec 2024 09:09:43 +0100 Subject: [PATCH 01/18] Use custom attribute to store blockquote depth on iOS (#564) --- apple/MarkdownLayoutManager.mm | 45 ++++++++++++++-------------------- apple/RCTMarkdownUtils.h | 3 ++- apple/RCTMarkdownUtils.mm | 7 +----- example/ios/Podfile.lock | 8 +++--- 4 files changed, 26 insertions(+), 37 deletions(-) diff --git a/apple/MarkdownLayoutManager.mm b/apple/MarkdownLayoutManager.mm index 3974ba98c..4ea7da64d 100644 --- a/apple/MarkdownLayoutManager.mm +++ b/apple/MarkdownLayoutManager.mm @@ -5,34 +5,27 @@ @implementation MarkdownLayoutManager - (void)drawBackgroundForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin { [super drawBackgroundForGlyphRange:glyphsToShow atPoint:origin]; + NSTextStorage *textStorage = self.textStorage; + [self enumerateLineFragmentsForGlyphRange:glyphsToShow usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer * _Nonnull textContainer, NSRange glyphRange, BOOL * _Nonnull stop) { - __block BOOL isBlockquote = NO; - __block int currentDepth = 0; + NSNumber *depth = [textStorage attribute:RCTLiveMarkdownBlockquoteDepthAttributeName atIndex:glyphRange.location effectiveRange:nil]; + if (depth == nil) { + return; // not a blockquote + } + 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; - } - }]; - 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); - } + 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 (NSUInteger level = 0; level < [depth unsignedIntValue]; level++) { + CGFloat x = paddingLeft + (level * shift) + markdownUtils.markdownStyle.blockquoteMarginLeft; + CGRect lineRect = CGRectMake(x, y, width, height); + [markdownUtils.markdownStyle.blockquoteBorderColor setFill]; + UIRectFill(lineRect); } }]; } diff --git a/apple/RCTMarkdownUtils.h b/apple/RCTMarkdownUtils.h index e942139d9..7b76d4382 100644 --- a/apple/RCTMarkdownUtils.h +++ b/apple/RCTMarkdownUtils.h @@ -3,11 +3,12 @@ NS_ASSUME_NONNULL_BEGIN +const NSAttributedStringKey RCTLiveMarkdownBlockquoteDepthAttributeName = @"RCTLiveMarkdownBlockquoteDepth"; + @interface RCTMarkdownUtils : NSObject @property (nonatomic) RCTMarkdownStyle *markdownStyle; @property (nonatomic) NSNumber *parserId; -@property (nonatomic) NSMutableArray *blockquoteRangesAndLevels; - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withAttributes:(nullable NSDictionary*)attributes; diff --git a/apple/RCTMarkdownUtils.mm b/apple/RCTMarkdownUtils.mm index 57d90774e..8a5ee4473 100644 --- a/apple/RCTMarkdownUtils.mm +++ b/apple/RCTMarkdownUtils.mm @@ -46,8 +46,6 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withA // 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)]; - _blockquoteRangesAndLevels = [NSMutableArray new]; - for (MarkdownRange *markdownRange in markdownRanges) { [self applyRangeToAttributedString:attributedString type:std::string([markdownRange.type UTF8String]) @@ -134,10 +132,7 @@ - (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedStri paragraphStyle.firstLineHeadIndent = indent; paragraphStyle.headIndent = indent; [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; - [_blockquoteRangesAndLevels addObject:@{ - @"range": [NSValue valueWithRange:range], - @"depth": @(depth) - }]; + [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; diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index d9e49e6f9..2a4b70a6c 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1497,7 +1497,7 @@ PODS: - React-logger (= 0.75.3) - React-perflogger (= 0.75.3) - React-utils (= 0.75.3) - - RNLiveMarkdown (0.1.190): + - RNLiveMarkdown (0.1.195): - DoubleConversion - glog - hermes-engine @@ -1517,10 +1517,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/newarch (= 0.1.190) + - RNLiveMarkdown/newarch (= 0.1.195) - RNReanimated/worklets - Yoga - - RNLiveMarkdown/newarch (0.1.190): + - RNLiveMarkdown/newarch (0.1.195): - DoubleConversion - glog - hermes-engine @@ -1897,7 +1897,7 @@ SPEC CHECKSUMS: React-utils: f2afa6acd905ca2ce7bb8ffb4a22f7f8a12534e8 ReactCodegen: e35c23cdd36922f6d2990c6c1f1b022ade7ad74d ReactCommon: 289214026502e6a93484f4a46bcc0efa4f3f2864 - RNLiveMarkdown: a210cbb45b6cb9db0b28ef09aafdc9c77424dd38 + RNLiveMarkdown: 18b4dec85110bc61b02f53501cd9e7aa08066b7f RNReanimated: ab6c33a61e90c4cbe5dbcbe65bd6c7cb3be167e6 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: 1354c027ab07c7736f99a3bef16172d6f1b12b47 From b39751078e19560e896f34ae8bbd43aabaf8b8cc Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 08:10:35 +0000 Subject: [PATCH 02/18] Update package-lock.json version to 0.1.198 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0756d1d2e..94d4ecf7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.197", + "version": "0.1.198", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.197", + "version": "0.1.198", "hasInstallScript": true, "license": "MIT", "workspaces": [ From 9ad0acdc7ffcd2d0be23fb64031bcac2ebed872e Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 08:10:36 +0000 Subject: [PATCH 03/18] Update package.json version to 0.1.198 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bf6bb60c5..b81b9a96f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.197", + "version": "0.1.198", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From 0391655bd909030b3a90a8315dce44abb28687b7 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Fri, 6 Dec 2024 23:59:31 +0100 Subject: [PATCH 04/18] Add systraces on Android (#565) --- .../livemarkdown/MarkdownFormatter.java | 34 +++++--- .../livemarkdown/MarkdownParser.java | 79 +++++++++++-------- .../expensify/livemarkdown/MarkdownUtils.java | 12 ++- 3 files changed, 80 insertions(+), 45 deletions(-) diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java index c7f683bf1..1fb1469ea 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java @@ -8,6 +8,7 @@ import com.expensify.livemarkdown.spans.*; import com.facebook.react.views.text.internal.span.CustomLineHeightSpan; +import com.facebook.systrace.Systrace; import java.util.List; import java.util.Objects; @@ -20,22 +21,37 @@ public MarkdownFormatter(@NonNull AssetManager assetManager) { } public void format(SpannableStringBuilder ssb, List markdownRanges, @NonNull MarkdownStyle markdownStyle) { - Objects.requireNonNull(markdownStyle, "mMarkdownStyle is null"); - removeSpans(ssb); - applyRanges(ssb, markdownRanges, markdownStyle); + try { + Systrace.beginSection(0, "format"); + Objects.requireNonNull(markdownStyle, "mMarkdownStyle is null"); + removeSpans(ssb); + applyRanges(ssb, markdownRanges, markdownStyle); + } finally { + Systrace.endSection(0); + } } private void removeSpans(SpannableStringBuilder ssb) { - // We shouldn't use `removeSpans()` because it also removes SpellcheckSpan, SuggestionSpan etc. - MarkdownSpan[] spans = ssb.getSpans(0, ssb.length(), MarkdownSpan.class); - for (MarkdownSpan span : spans) { - ssb.removeSpan(span); + try { + Systrace.beginSection(0, "removeSpans"); + // We shouldn't use `removeSpans()` because it also removes SpellcheckSpan, SuggestionSpan etc. + MarkdownSpan[] spans = ssb.getSpans(0, ssb.length(), MarkdownSpan.class); + for (MarkdownSpan span : spans) { + ssb.removeSpan(span); + } + } finally { + Systrace.endSection(0); } } private void applyRanges(SpannableStringBuilder ssb, List markdownRanges, @NonNull MarkdownStyle markdownStyle) { - for (MarkdownRange markdownRange : markdownRanges) { - applyRange(ssb, markdownRange, markdownStyle); + try { + Systrace.beginSection(0, "applyRanges"); + for (MarkdownRange markdownRange : markdownRanges) { + applyRange(ssb, markdownRange, markdownStyle); + } + } finally { + Systrace.endSection(0); } } diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java index e79963bbd..292c4e13e 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java @@ -5,6 +5,7 @@ import com.facebook.react.bridge.ReactContext; import com.facebook.react.util.RNLog; import com.facebook.soloader.SoLoader; +import com.facebook.systrace.Systrace; import org.json.JSONArray; import org.json.JSONException; @@ -31,46 +32,58 @@ public MarkdownParser(@NonNull ReactContext reactContext) { private native String nativeParse(String text, int parserId); public synchronized List parse(String text, int parserId) { - if (text.equals(mPrevText) && parserId == mPrevParserId) { - return mPrevMarkdownRanges; - } - - String json; try { - json = nativeParse(text, parserId); - } catch (Exception e) { - // Skip formatting, runGuarded will show the error in LogBox - mPrevText = text; - mPrevParserId = parserId; - mPrevMarkdownRanges = Collections.emptyList(); - return mPrevMarkdownRanges; - } + Systrace.beginSection(0, "parse"); - List markdownRanges = new LinkedList<>(); - try { - JSONArray ranges = new JSONArray(json); - for (int i = 0; i < ranges.length(); i++) { - JSONObject range = ranges.getJSONObject(i); - String type = range.getString("type"); - int start = range.getInt("start"); - int length = range.getInt("length"); - int depth = range.optInt("depth", 1); - if (length == 0 || start + length > text.length()) { - continue; + if (text.equals(mPrevText) && parserId == mPrevParserId) { + return mPrevMarkdownRanges; + } + + String json; + try { + Systrace.beginSection(0, "nativeParse"); + json = nativeParse(text, parserId); + } catch (Exception e) { + // Skip formatting, runGuarded will show the error in LogBox + mPrevText = text; + mPrevParserId = parserId; + mPrevMarkdownRanges = Collections.emptyList(); + return mPrevMarkdownRanges; + } finally { + Systrace.endSection(0); + } + + List markdownRanges = new LinkedList<>(); + try { + Systrace.beginSection(0, "markdownRanges"); + JSONArray ranges = new JSONArray(json); + for (int i = 0; i < ranges.length(); i++) { + JSONObject range = ranges.getJSONObject(i); + String type = range.getString("type"); + int start = range.getInt("start"); + int length = range.getInt("length"); + int depth = range.optInt("depth", 1); + if (length == 0 || start + length > text.length()) { + continue; + } + markdownRanges.add(new MarkdownRange(type, start, length, depth)); } - markdownRanges.add(new MarkdownRange(type, start, length, depth)); + } catch (JSONException e) { + RNLog.w(mReactContext, "[react-native-live-markdown] Incorrect schema of worklet parser output: " + e.getMessage()); + mPrevText = text; + mPrevParserId = parserId; + mPrevMarkdownRanges = Collections.emptyList(); + return mPrevMarkdownRanges; + } finally { + Systrace.endSection(0); } - } catch (JSONException e) { - RNLog.w(mReactContext, "[react-native-live-markdown] Incorrect schema of worklet parser output: " + e.getMessage()); + mPrevText = text; mPrevParserId = parserId; - mPrevMarkdownRanges = Collections.emptyList(); + mPrevMarkdownRanges = markdownRanges; return mPrevMarkdownRanges; + } finally { + Systrace.endSection(0); } - - mPrevText = text; - mPrevParserId = parserId; - mPrevMarkdownRanges = markdownRanges; - return mPrevMarkdownRanges; } } diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java index be72ec4ac..6877e46f9 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java @@ -5,6 +5,7 @@ import androidx.annotation.NonNull; import com.facebook.react.bridge.ReactContext; +import com.facebook.systrace.Systrace; import java.util.List; @@ -29,8 +30,13 @@ public void setParserId(int parserId) { } public void applyMarkdownFormatting(SpannableStringBuilder ssb) { - String text = ssb.toString(); - List markdownRanges = mMarkdownParser.parse(text, mParserId); - mMarkdownFormatter.format(ssb, markdownRanges, mMarkdownStyle); + try { + Systrace.beginSection(0, "applyMarkdownFormatting"); + String text = ssb.toString(); + List markdownRanges = mMarkdownParser.parse(text, mParserId); + mMarkdownFormatter.format(ssb, markdownRanges, mMarkdownStyle); + } finally { + Systrace.endSection(0); + } } } From c02d2611ce219b9dcbde84dcf95c62bbe913e051 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 23:00:22 +0000 Subject: [PATCH 05/18] Update package-lock.json version to 0.1.199 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 94d4ecf7e..7ea74a30f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.198", + "version": "0.1.199", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.198", + "version": "0.1.199", "hasInstallScript": true, "license": "MIT", "workspaces": [ From 5695408fe6286724276438e4e957ed2a6b2101c3 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 23:00:23 +0000 Subject: [PATCH 06/18] Update package.json version to 0.1.199 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b81b9a96f..38407d38b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.198", + "version": "0.1.199", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From 3822c88bccd9b90230ba907c3b68ec9e77ca4c48 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Mon, 9 Dec 2024 10:47:21 +0100 Subject: [PATCH 07/18] Use pattern matching for instanceof in `afterTextChanged` (#567) --- .../java/com/expensify/livemarkdown/MarkdownTextWatcher.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownTextWatcher.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownTextWatcher.java index 23b215a23..0e320cd7b 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownTextWatcher.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownTextWatcher.java @@ -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); } } } From ae4d77fb15d0161f4abb3f7d8d418e2224f26ec1 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 09:48:18 +0000 Subject: [PATCH 08/18] Update package-lock.json version to 0.1.200 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7ea74a30f..068a83a57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.199", + "version": "0.1.200", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.199", + "version": "0.1.200", "hasInstallScript": true, "license": "MIT", "workspaces": [ From bb20e3ac91b0a7cc150a2a0a7f73d9961d1e70d5 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 09:48:19 +0000 Subject: [PATCH 09/18] Update package.json version to 0.1.200 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 38407d38b..bd0743ade 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.199", + "version": "0.1.200", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From 83cdfea352be8e76bae56e306378296637aebaf4 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Mon, 9 Dec 2024 10:57:28 +0100 Subject: [PATCH 10/18] Add `@NonNull` annotations (#569) --- .../com/expensify/livemarkdown/MarkdownFormatter.java | 10 +++++----- .../com/expensify/livemarkdown/MarkdownParser.java | 4 ++-- .../java/com/expensify/livemarkdown/MarkdownRange.java | 6 ++++-- .../java/com/expensify/livemarkdown/MarkdownStyle.java | 2 ++ .../expensify/livemarkdown/MarkdownTextWatcher.java | 2 +- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java index 1fb1469ea..95760be87 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java @@ -20,7 +20,7 @@ public MarkdownFormatter(@NonNull AssetManager assetManager) { mAssetManager = assetManager; } - public void format(SpannableStringBuilder ssb, List markdownRanges, @NonNull MarkdownStyle markdownStyle) { + public void format(@NonNull SpannableStringBuilder ssb, @NonNull List markdownRanges, @NonNull MarkdownStyle markdownStyle) { try { Systrace.beginSection(0, "format"); Objects.requireNonNull(markdownStyle, "mMarkdownStyle is null"); @@ -31,7 +31,7 @@ public void format(SpannableStringBuilder ssb, List 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. @@ -44,7 +44,7 @@ private void removeSpans(SpannableStringBuilder ssb) { } } - private void applyRanges(SpannableStringBuilder ssb, List markdownRanges, @NonNull MarkdownStyle markdownStyle) { + private void applyRanges(@NonNull SpannableStringBuilder ssb, @NonNull List markdownRanges, @NonNull MarkdownStyle markdownStyle) { try { Systrace.beginSection(0, "applyRanges"); for (MarkdownRange markdownRange : markdownRanges) { @@ -55,7 +55,7 @@ private void applyRanges(SpannableStringBuilder ssb, List 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(); @@ -126,7 +126,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); } } diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java index 292c4e13e..3e108db70 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java @@ -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 parse(String text, int parserId) { + public synchronized List parse(@NonNull String text, int parserId) { try { Systrace.beginSection(0, "parse"); diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownRange.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownRange.java index 9c3aedd3d..337f1b4a6 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownRange.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownRange.java @@ -1,12 +1,14 @@ 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 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; mLength = length; diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java index cf691e62a..50606f982 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java @@ -32,6 +32,7 @@ public class MarkdownStyle { private final float mBlockquotePaddingLeft; + @NonNull private final String mCodeFontFamily; private final float mCodeFontSize; @@ -42,6 +43,7 @@ public class MarkdownStyle { @ColorInt private final int mCodeBackgroundColor; + @NonNull private final String mPreFontFamily; private final float mPreFontSize; diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownTextWatcher.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownTextWatcher.java index 0e320cd7b..d182be1fd 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownTextWatcher.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownTextWatcher.java @@ -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; From 1336cfdc26b45e67c081a82ee6afa94ebe2ea0c5 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 09:58:19 +0000 Subject: [PATCH 11/18] Update package-lock.json version to 0.1.201 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 068a83a57..e6cdbdbd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.200", + "version": "0.1.201", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.200", + "version": "0.1.201", "hasInstallScript": true, "license": "MIT", "workspaces": [ From c452bb49a1e128678495799d65c50703d0c17f3e Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 09:58:21 +0000 Subject: [PATCH 12/18] Update package.json version to 0.1.201 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bd0743ade..099c93400 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.200", + "version": "0.1.201", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From f2ef3256456f930d1500df8926f3dd0940123ef7 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Mon, 9 Dec 2024 11:12:31 +0100 Subject: [PATCH 13/18] Add `getEnd()` method in `MarkdownRange` (#570) --- .../java/com/expensify/livemarkdown/MarkdownFormatter.java | 2 +- .../main/java/com/expensify/livemarkdown/MarkdownRange.java | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java index 95760be87..2ef8ccf97 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java @@ -58,7 +58,7 @@ private void applyRanges(@NonNull SpannableStringBuilder ssb, @NonNull List Date: Mon, 9 Dec 2024 10:13:22 +0000 Subject: [PATCH 14/18] Update package-lock.json version to 0.1.202 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index e6cdbdbd6..4f4832ddc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.201", + "version": "0.1.202", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.201", + "version": "0.1.202", "hasInstallScript": true, "license": "MIT", "workspaces": [ From 4fbf007eea02d40655cefab0467a02c131b00275 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:13:23 +0000 Subject: [PATCH 15/18] Update package.json version to 0.1.202 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 099c93400..dd0d82ce7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.201", + "version": "0.1.202", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From 0f9011bc155b858ba83d1d3d132c1798615067ee Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Mon, 9 Dec 2024 11:19:09 +0100 Subject: [PATCH 16/18] Move formatting logic to separate class on iOS (#572) --- apple/MarkdownFormatter.h | 18 ++++ apple/MarkdownFormatter.mm | 158 ++++++++++++++++++++++++++++++ apple/MarkdownLayoutManager.h | 1 + apple/RCTMarkdownUtils.h | 2 - apple/RCTMarkdownUtils.mm | 174 ++++------------------------------ example/ios/Podfile.lock | 8 +- 6 files changed, 201 insertions(+), 160 deletions(-) create mode 100644 apple/MarkdownFormatter.h create mode 100644 apple/MarkdownFormatter.mm diff --git a/apple/MarkdownFormatter.h b/apple/MarkdownFormatter.h new file mode 100644 index 000000000..1f5b1a2e1 --- /dev/null +++ b/apple/MarkdownFormatter.h @@ -0,0 +1,18 @@ +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +const NSAttributedStringKey RCTLiveMarkdownBlockquoteDepthAttributeName = @"RCTLiveMarkdownBlockquoteDepth"; + +@interface MarkdownFormatter : NSObject + +- (nonnull NSAttributedString *)format:(nonnull NSString *)text + withAttributes:(nullable NSDictionary*)attributes + withMarkdownRanges:(nonnull NSArray *)markdownRanges + withMarkdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle; + +NS_ASSUME_NONNULL_END + +@end diff --git a/apple/MarkdownFormatter.mm b/apple/MarkdownFormatter.mm new file mode 100644 index 000000000..642b487bb --- /dev/null +++ b/apple/MarkdownFormatter.mm @@ -0,0 +1,158 @@ +#import "MarkdownFormatter.h" +#import + +@implementation MarkdownFormatter + +- (nonnull NSAttributedString *)format:(nonnull NSString *)text + withAttributes:(nullable NSDictionary *)attributes + withMarkdownRanges:(nonnull NSArray *)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 diff --git a/apple/MarkdownLayoutManager.h b/apple/MarkdownLayoutManager.h index 29be3508d..9e965e113 100644 --- a/apple/MarkdownLayoutManager.h +++ b/apple/MarkdownLayoutManager.h @@ -1,5 +1,6 @@ #import #import +#import NS_ASSUME_NONNULL_BEGIN diff --git a/apple/RCTMarkdownUtils.h b/apple/RCTMarkdownUtils.h index 7b76d4382..ea1264722 100644 --- a/apple/RCTMarkdownUtils.h +++ b/apple/RCTMarkdownUtils.h @@ -3,8 +3,6 @@ NS_ASSUME_NONNULL_BEGIN -const NSAttributedStringKey RCTLiveMarkdownBlockquoteDepthAttributeName = @"RCTLiveMarkdownBlockquoteDepth"; - @interface RCTMarkdownUtils : NSObject @property (nonatomic) RCTMarkdownStyle *markdownStyle; diff --git a/apple/RCTMarkdownUtils.mm b/apple/RCTMarkdownUtils.mm index 8a5ee4473..3c90238be 100644 --- a/apple/RCTMarkdownUtils.mm +++ b/apple/RCTMarkdownUtils.mm @@ -1,13 +1,10 @@ #import -#import #import -#import "react_native_assert.h" -#import -#import -#include +#import @implementation RCTMarkdownUtils { MarkdownParser *_markdownParser; + MarkdownFormatter *_markdownFormatter; NSString *_prevInputString; NSAttributedString *_prevAttributedString; NSDictionary *_prevTextAttributes; @@ -19,6 +16,7 @@ - (instancetype)init { if (self = [super init]) { _markdownParser = [MarkdownParser new]; + _markdownFormatter = [MarkdownFormatter new]; } return self; @@ -26,162 +24,30 @@ - (instancetype)init - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withAttributes:(nullable NSDictionary *)attributes { - @synchronized (self) { - if (input == nil) { - return nil; - } - - NSString *inputString = [input string]; - if ([inputString isEqualToString:_prevInputString] && [attributes isEqualToDictionary:_prevTextAttributes] && [_markdownStyle isEqual:_prevMarkdownStyle] && [_parserId isEqualToNumber:_prevParserId]) { - return _prevAttributedString; - } - - NSArray *markdownRanges = [_markdownParser parse:inputString withParserId:_parserId]; - - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:inputString 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]; - } - - RCTApplyBaselineOffset(attributedString); - - [attributedString endEditing]; - - _prevInputString = inputString; - _prevAttributedString = attributedString; - _prevTextAttributes = attributes; - _prevMarkdownStyle = _markdownStyle; - _prevParserId = _parserId; - - return attributedString; + @synchronized (self) { + if (input == nil) { + return nil; } -} -- (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedString type:(const std::string)type range:(NSRange)range depth:(const int)depth { - 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]; + NSString *inputString = [input string]; + if ([inputString isEqualToString:_prevInputString] && [attributes isEqualToDictionary:_prevTextAttributes] && [_markdownStyle isEqual:_prevMarkdownStyle] && [_parserId isEqualToNumber:_prevParserId]) { + return _prevAttributedString; } - 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 - } -} + NSArray *markdownRanges = [_markdownParser parse:inputString withParserId:_parserId]; -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; - } + NSAttributedString *attributedString = [_markdownFormatter format:inputString + withAttributes:attributes + withMarkdownRanges:markdownRanges + withMarkdownStyle:_markdownStyle]; + _prevInputString = inputString; + _prevAttributedString = attributedString; + _prevTextAttributes = attributes; + _prevMarkdownStyle = _markdownStyle; + _prevParserId = _parserId; - maximumLineHeight = MAX(paragraphStyle.maximumLineHeight, maximumLineHeight); - }]; - - if (maximumLineHeight == 0) { - // `lineHeight` was not specified, nothing to do. - return; + return attributedString; } - - __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 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 2a4b70a6c..f3bb3529c 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1497,7 +1497,7 @@ PODS: - React-logger (= 0.75.3) - React-perflogger (= 0.75.3) - React-utils (= 0.75.3) - - RNLiveMarkdown (0.1.195): + - RNLiveMarkdown (0.1.199): - DoubleConversion - glog - hermes-engine @@ -1517,10 +1517,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/newarch (= 0.1.195) + - RNLiveMarkdown/newarch (= 0.1.199) - RNReanimated/worklets - Yoga - - RNLiveMarkdown/newarch (0.1.195): + - RNLiveMarkdown/newarch (0.1.199): - DoubleConversion - glog - hermes-engine @@ -1897,7 +1897,7 @@ SPEC CHECKSUMS: React-utils: f2afa6acd905ca2ce7bb8ffb4a22f7f8a12534e8 ReactCodegen: e35c23cdd36922f6d2990c6c1f1b022ade7ad74d ReactCommon: 289214026502e6a93484f4a46bcc0efa4f3f2864 - RNLiveMarkdown: 18b4dec85110bc61b02f53501cd9e7aa08066b7f + RNLiveMarkdown: 18dd4ceada29d66a6b7c29b1b0df589e2fc82183 RNReanimated: ab6c33a61e90c4cbe5dbcbe65bd6c7cb3be167e6 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: 1354c027ab07c7736f99a3bef16172d6f1b12b47 From b441d0bfbe765a79469baaada8314251261094ea Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:19:57 +0000 Subject: [PATCH 17/18] Update package-lock.json version to 0.1.203 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f4832ddc..3b9507a72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.202", + "version": "0.1.203", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.202", + "version": "0.1.203", "hasInstallScript": true, "license": "MIT", "workspaces": [ From c35696d8a65ffe06d1acc066a9ff2a17b01eb77d Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:19:58 +0000 Subject: [PATCH 18/18] Update package.json version to 0.1.203 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dd0d82ce7..0a705c956 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.202", + "version": "0.1.203", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index",