From 3c17bf421db69d26197886c35f5f7a5e38131a6d Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 5 Dec 2024 11:46:29 +0100 Subject: [PATCH 01/75] bump expensify-common to 2.0.108 (#557) --- README.md | 2 +- example/package.json | 2 +- package-lock.json | 13 +++++++------ package.json | 4 ++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e5c2c7ef..d8c2dec5 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ npm install @expensify/react-native-live-markdown react-native-reanimated expens npx expo install @expensify/react-native-live-markdown react-native-reanimated expensify-common ``` -React Native Live Markdown requires [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated) 3.16.3 or newer and [expensify-common](https://github.com/Expensify/expensify-common) 2.0.106 or newer. +React Native Live Markdown requires [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated) 3.16.3 or newer and [expensify-common](https://github.com/Expensify/expensify-common) 2.0.108 or newer. Then, install the iOS dependencies with CocoaPods: diff --git a/example/package.json b/example/package.json index 268543b5..c75e8e09 100644 --- a/example/package.json +++ b/example/package.json @@ -9,7 +9,7 @@ "start": "react-native start" }, "dependencies": { - "expensify-common": "2.0.106", + "expensify-common": "2.0.108", "react": "18.3.1", "react-native": "0.75.3", "react-native-reanimated": "3.16.3" diff --git a/package-lock.json b/package-lock.json index 043a4b13..0ccd6329 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-tsdoc": "^0.2.17", - "expensify-common": "2.0.106", + "expensify-common": "2.0.108", "jest": "^29.6.3", "jest-environment-jsdom": "^29.7.0", "nodemon": "^3.1.3", @@ -58,7 +58,7 @@ "node": ">= 18.0.0" }, "peerDependencies": { - "expensify-common": ">=2.0.106", + "expensify-common": ">=2.0.108", "react": "*", "react-native": "*", "react-native-reanimated": ">=3.16.3" @@ -68,7 +68,7 @@ "name": "@expensify/react-native-live-markdown-example", "version": "0.0.1", "dependencies": { - "expensify-common": "2.0.106", + "expensify-common": "2.0.108", "react": "18.3.1", "react-native": "0.75.3", "react-native-reanimated": "3.16.3" @@ -14461,9 +14461,10 @@ } }, "node_modules/expensify-common": { - "version": "2.0.106", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.106.tgz", - "integrity": "sha512-KmxKvglbIUJb0sAcmNxb/AXYAqa3GIZfu3MbmtlYDNJx24mjDjtbGkKhm+16TICDoPj2PDRNogIqgUGWmSSZFQ==", + "version": "2.0.108", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.108.tgz", + "integrity": "sha512-q4chHq1dxHJP/5CRTmTiIRrQKZik0Ms2yM6kjCMlCuJT/3aTlFOHCChb24TB/7TgjbSw0utj1cnFhRDfL87RWg==", + "license": "MIT", "dependencies": { "awesome-phonenumber": "^5.4.0", "classnames": "2.5.0", diff --git a/package.json b/package.json index f5dec0da..372febf8 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-tsdoc": "^0.2.17", - "expensify-common": "2.0.106", + "expensify-common": "2.0.108", "jest": "^29.6.3", "jest-environment-jsdom": "^29.7.0", "nodemon": "^3.1.3", @@ -100,7 +100,7 @@ "typescript": "~5.3.3" }, "peerDependencies": { - "expensify-common": ">=2.0.106", + "expensify-common": ">=2.0.108", "react": "*", "react-native": "*", "react-native-reanimated": ">=3.16.3" From 08457f1d0ca62ef74a9220f85f549397eee7bf8d Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:47:22 +0000 Subject: [PATCH 02/75] Update package-lock.json version to 0.1.192 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0ccd6329..f46aedb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.191", + "version": "0.1.192", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.191", + "version": "0.1.192", "hasInstallScript": true, "license": "MIT", "workspaces": [ From 67dd5d18ac83f98ba45854dcd38f286a5059e88f Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:47:23 +0000 Subject: [PATCH 03/75] Update package.json version to 0.1.192 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 372febf8..15b4058e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.191", + "version": "0.1.192", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From 02793be543357fd026fc4374e8a64bf66e14b772 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Thu, 5 Dec 2024 11:50:31 +0100 Subject: [PATCH 04/75] Move parsing logic to separate class on iOS (#558) --- apple/MarkdownParser.h | 12 +++++++ apple/MarkdownParser.mm | 68 +++++++++++++++++++++++++++++++++++++++ apple/RCTMarkdownUtils.mm | 51 ++++++++--------------------- example/ios/Podfile.lock | 8 ++--- 4 files changed, 97 insertions(+), 42 deletions(-) create mode 100644 apple/MarkdownParser.h create mode 100644 apple/MarkdownParser.mm diff --git a/apple/MarkdownParser.h b/apple/MarkdownParser.h new file mode 100644 index 00000000..7ec8d195 --- /dev/null +++ b/apple/MarkdownParser.h @@ -0,0 +1,12 @@ +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface MarkdownParser : NSObject + +- (NSArray *)parse:(NSString *)text withParserId:(NSNumber *)parserId; + +NS_ASSUME_NONNULL_END + +@end diff --git a/apple/MarkdownParser.mm b/apple/MarkdownParser.mm new file mode 100644 index 00000000..c9f3d018 --- /dev/null +++ b/apple/MarkdownParser.mm @@ -0,0 +1,68 @@ +#import "MarkdownParser.h" +#import +#import +#import + +@implementation MarkdownParser { + NSString *_prevText; + NSNumber *_prevParserId; + NSArray *_prevMarkdownRanges; +} + +- (NSArray *)parse:(NSString *)text withParserId:(nonnull NSNumber *)parserId { + @synchronized (self) { + if ([text isEqualToString:_prevText] && [parserId isEqualToNumber:_prevParserId]) { + return _prevMarkdownRanges; + } + + static std::mutex workletRuntimeMutex; // this needs to be global since the worklet runtime is also global + const auto lock = std::lock_guard(workletRuntimeMutex); + + const auto &markdownRuntime = expensify::livemarkdown::getMarkdownRuntime(); + jsi::Runtime &rt = markdownRuntime->getJSIRuntime(); + + const auto &markdownWorklet = expensify::livemarkdown::getMarkdownWorklet([parserId intValue]); + + const auto &input = jsi::String::createFromUtf8(rt, [text UTF8String]); + + jsi::Value output; + try { + output = markdownRuntime->runGuarded(markdownWorklet, input); + } catch (const jsi::JSError &error) { + // Skip formatting, runGuarded will show the error in LogBox + _prevText = text; + _prevParserId = parserId; + _prevMarkdownRanges = @[]; + return _prevMarkdownRanges; + } + + NSMutableArray *markdownRanges = [[NSMutableArray alloc] init]; + try { + const auto &ranges = output.asObject(rt).asArray(rt); + for (size_t i = 0, n = ranges.size(rt); i < n; ++i) { + const auto &item = ranges.getValueAtIndex(rt, i).asObject(rt); + const auto &type = item.getProperty(rt, "type").asString(rt).utf8(rt); + const auto &start = static_cast(item.getProperty(rt, "start").asNumber()); + const auto &length = static_cast(item.getProperty(rt, "length").asNumber()); + const auto &depth = item.hasProperty(rt, "depth") ? static_cast(item.getProperty(rt, "depth").asNumber()) : 1; + + NSRange range = NSMakeRange(start, length); + MarkdownRange *markdownRange = [[MarkdownRange alloc] initWithType:@(type.c_str()) range:range depth:depth]; + [markdownRanges addObject:markdownRange]; + } + } catch (const jsi::JSError &error) { + RCTLogWarn(@"[react-native-live-markdown] Incorrect schema of worklet parser output: %s", error.getMessage().c_str()); + _prevText = text; + _prevParserId = parserId; + _prevMarkdownRanges = @[]; + return _prevMarkdownRanges; + } + + _prevText = text; + _prevParserId = parserId; + _prevMarkdownRanges = markdownRanges; + return _prevMarkdownRanges; + } +} + +@end diff --git a/apple/RCTMarkdownUtils.mm b/apple/RCTMarkdownUtils.mm index c9ceed1c..11a20bca 100644 --- a/apple/RCTMarkdownUtils.mm +++ b/apple/RCTMarkdownUtils.mm @@ -1,12 +1,13 @@ #import #import -#import +#import #import "react_native_assert.h" #import #import #include @implementation RCTMarkdownUtils { + MarkdownParser *_markdownParser; NSString *_prevInputString; NSAttributedString *_prevAttributedString; NSDictionary *_prevTextAttributes; @@ -14,6 +15,15 @@ @implementation RCTMarkdownUtils { __weak NSNumber *_prevParserId; } +- (instancetype)init +{ + if (self = [super init]) { + _markdownParser = [MarkdownParser new]; + } + + return self; +} + - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withAttributes:(nullable NSDictionary *)attributes { @synchronized (self) { @@ -26,43 +36,8 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withA return _prevAttributedString; } - static std::mutex runtimeMutex; - auto lock = std::lock_guard(runtimeMutex); - - auto markdownRuntime = expensify::livemarkdown::getMarkdownRuntime(); - jsi::Runtime &rt = markdownRuntime->getJSIRuntime(); - - auto markdownWorklet = expensify::livemarkdown::getMarkdownWorklet([_parserId intValue]); - - NSMutableArray *markdownRanges = [[NSMutableArray alloc] init]; - - try { - const auto &text = jsi::String::createFromUtf8(rt, [inputString UTF8String]); - const auto &output = markdownRuntime->runGuarded(markdownWorklet, text); - const auto &ranges = output.asObject(rt).asArray(rt); - - for (size_t i = 0, n = ranges.size(rt); i < n; ++i) { - const auto &item = ranges.getValueAtIndex(rt, i).asObject(rt); - const auto &type = item.getProperty(rt, "type").asString(rt).utf8(rt); - const auto &start = static_cast(item.getProperty(rt, "start").asNumber()); - const auto &length = static_cast(item.getProperty(rt, "length").asNumber()); - const auto &depth = item.hasProperty(rt, "depth") ? static_cast(item.getProperty(rt, "depth").asNumber()) : 1; - - NSRange range = NSMakeRange(start, length); - MarkdownRange *markdownRange = [[MarkdownRange alloc] initWithType:@(type.c_str()) range:range depth:depth]; - [markdownRanges addObject:markdownRange]; - } - } catch (const jsi::JSError &error) { - RCTLogWarn(@"[react-native-live-markdown] Incorrect schema of worklet parser output: %s", error.getMessage().c_str()); - NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:inputString attributes:attributes]; - _prevInputString = inputString; - _prevAttributedString = attributedString; - _prevTextAttributes = attributes; - _prevMarkdownStyle = _markdownStyle; - _prevParserId = _parserId; - return attributedString; - } - + NSArray *markdownRanges = [_markdownParser parse:inputString withParserId:_parserId]; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:inputString attributes:attributes]; [attributedString beginEditing]; diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 3a061a0a..d9e49e6f 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.188): + - RNLiveMarkdown (0.1.190): - DoubleConversion - glog - hermes-engine @@ -1517,10 +1517,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/newarch (= 0.1.188) + - RNLiveMarkdown/newarch (= 0.1.190) - RNReanimated/worklets - Yoga - - RNLiveMarkdown/newarch (0.1.188): + - RNLiveMarkdown/newarch (0.1.190): - DoubleConversion - glog - hermes-engine @@ -1897,7 +1897,7 @@ SPEC CHECKSUMS: React-utils: f2afa6acd905ca2ce7bb8ffb4a22f7f8a12534e8 ReactCodegen: e35c23cdd36922f6d2990c6c1f1b022ade7ad74d ReactCommon: 289214026502e6a93484f4a46bcc0efa4f3f2864 - RNLiveMarkdown: c0d3ebfa32b4a6a33f1dbfc76ab9a06e516bfb1a + RNLiveMarkdown: a210cbb45b6cb9db0b28ef09aafdc9c77424dd38 RNReanimated: ab6c33a61e90c4cbe5dbcbe65bd6c7cb3be167e6 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: 1354c027ab07c7736f99a3bef16172d6f1b12b47 From 76eff7be4b07163cb9d97b4eccfd132593879d81 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:51:23 +0000 Subject: [PATCH 05/75] Update package-lock.json version to 0.1.193 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f46aedb1..fa4f787e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.192", + "version": "0.1.193", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.192", + "version": "0.1.193", "hasInstallScript": true, "license": "MIT", "workspaces": [ From dcb21de1923ade8df58c256e768bffb8e22932b5 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:51:24 +0000 Subject: [PATCH 06/75] Update package.json version to 0.1.193 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 15b4058e..96e93120 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.192", + "version": "0.1.193", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From 790c02c6889676960318bdbc906be485fa25f23b Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Thu, 5 Dec 2024 14:43:10 +0100 Subject: [PATCH 07/75] Move parsing logic to separate class on Android (#560) --- android/src/main/cpp/MarkdownParser.cpp | 35 +++++++++ .../cpp/{MarkdownUtils.h => MarkdownParser.h} | 10 +-- android/src/main/cpp/MarkdownUtils.cpp | 33 --------- android/src/main/cpp/OnLoad.cpp | 4 +- .../livemarkdown/MarkdownParser.java | 71 +++++++++++++++++++ .../expensify/livemarkdown/MarkdownUtils.java | 51 ++----------- 6 files changed, 117 insertions(+), 87 deletions(-) create mode 100644 android/src/main/cpp/MarkdownParser.cpp rename android/src/main/cpp/{MarkdownUtils.h => MarkdownParser.h} (71%) delete mode 100644 android/src/main/cpp/MarkdownUtils.cpp create mode 100644 android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java diff --git a/android/src/main/cpp/MarkdownParser.cpp b/android/src/main/cpp/MarkdownParser.cpp new file mode 100644 index 00000000..b645eb48 --- /dev/null +++ b/android/src/main/cpp/MarkdownParser.cpp @@ -0,0 +1,35 @@ +#include "MarkdownParser.h" +#include "MarkdownGlobal.h" + +#include + +using namespace facebook; + +namespace expensify { +namespace livemarkdown { + jni::local_ref MarkdownParser::nativeParse( + jni::alias_ref jThis, + jni::alias_ref text, + const int parserId) { + static std::mutex workletRuntimeMutex; // this needs to be global since the worklet runtime is also global + const auto lock = std::lock_guard(workletRuntimeMutex); + + const auto markdownRuntime = expensify::livemarkdown::getMarkdownRuntime(); + jsi::Runtime &rt = markdownRuntime->getJSIRuntime(); + + const auto markdownWorklet = expensify::livemarkdown::getMarkdownWorklet(parserId); + + const auto input = jsi::String::createFromUtf8(rt, text->toStdString()); + const auto output = markdownRuntime->runGuarded(markdownWorklet, input); + + const auto json = rt.global().getPropertyAsObject(rt, "JSON").getPropertyAsFunction(rt, "stringify").call(rt, output).asString(rt).utf8(rt); + return jni::make_jstring(json); + } + + void MarkdownParser::registerNatives() { + registerHybrid({ + makeNativeMethod("nativeParse", MarkdownParser::nativeParse)}); + } + +} // namespace livemarkdown +} // namespace expensify diff --git a/android/src/main/cpp/MarkdownUtils.h b/android/src/main/cpp/MarkdownParser.h similarity index 71% rename from android/src/main/cpp/MarkdownUtils.h rename to android/src/main/cpp/MarkdownParser.h index 4a509b87..fc3313b5 100644 --- a/android/src/main/cpp/MarkdownUtils.h +++ b/android/src/main/cpp/MarkdownParser.h @@ -15,16 +15,16 @@ using namespace facebook; namespace expensify { namespace livemarkdown { - class MarkdownUtils : public jni::HybridClass, + class MarkdownParser : public jni::HybridClass, public jsi::HostObject { public: static constexpr auto kJavaDescriptor = - "Lcom/expensify/livemarkdown/MarkdownUtils;"; + "Lcom/expensify/livemarkdown/MarkdownParser;"; - static jni::local_ref nativeParseMarkdown( + static jni::local_ref nativeParse( jni::alias_ref jThis, - jni::alias_ref input, - int parserId); + jni::alias_ref text, + const int parserId); static void registerNatives(); diff --git a/android/src/main/cpp/MarkdownUtils.cpp b/android/src/main/cpp/MarkdownUtils.cpp deleted file mode 100644 index 9621e46f..00000000 --- a/android/src/main/cpp/MarkdownUtils.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include "MarkdownUtils.h" -#include "MarkdownGlobal.h" - -#include - -using namespace facebook; - -namespace expensify { -namespace livemarkdown { - jni::local_ref MarkdownUtils::nativeParseMarkdown( - jni::alias_ref jThis, - jni::alias_ref input, - int parserId) { - // This method is synchronized (see MarkdownUtils.java) so we don't need a mutex here. - const auto markdownRuntime = expensify::livemarkdown::getMarkdownRuntime(); - jsi::Runtime &rt = markdownRuntime->getJSIRuntime(); - - const auto markdownWorklet = expensify::livemarkdown::getMarkdownWorklet(parserId); - - const auto text = jsi::String::createFromUtf8(rt, input->toStdString()); - const auto result = markdownRuntime->runGuarded(markdownWorklet, text); - - const auto json = rt.global().getPropertyAsObject(rt, "JSON").getPropertyAsFunction(rt, "stringify").call(rt, result).asString(rt).utf8(rt); - return jni::make_jstring(json); - } - - void MarkdownUtils::registerNatives() { - registerHybrid({ - makeNativeMethod("nativeParseMarkdown", MarkdownUtils::nativeParseMarkdown)}); - } - -} // namespace livemarkdown -} // namespace expensify diff --git a/android/src/main/cpp/OnLoad.cpp b/android/src/main/cpp/OnLoad.cpp index 47086340..7daa4a33 100644 --- a/android/src/main/cpp/OnLoad.cpp +++ b/android/src/main/cpp/OnLoad.cpp @@ -1,11 +1,11 @@ #include -#include "MarkdownUtils.h" +#include "MarkdownParser.h" #include "RuntimeDecorator.h" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { return facebook::jni::initialize( - vm, [] { expensify::livemarkdown::MarkdownUtils::registerNatives(); }); + vm, [] { expensify::livemarkdown::MarkdownParser::registerNatives(); }); } extern "C" JNIEXPORT void JNICALL Java_com_expensify_livemarkdown_LiveMarkdownModule_injectJSIBindings(JNIEnv *env, jobject thiz, jlong jsiRuntime) { diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java new file mode 100644 index 00000000..17bd0e71 --- /dev/null +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java @@ -0,0 +1,71 @@ +package com.expensify.livemarkdown; + +import androidx.annotation.NonNull; + +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.util.RNLog; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +public class MarkdownParser { + private final @NonNull ReactContext mReactContext; + private String mPrevText; + private int mPrevParserId; + private List mPrevMarkdownRanges; + + public MarkdownParser(@NonNull ReactContext reactContext) { + mReactContext = 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; + } + + 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; + } + 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; + } + + 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 3451fdb4..abb213d0 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java @@ -8,15 +8,9 @@ import com.expensify.livemarkdown.spans.*; import com.facebook.react.bridge.ReactContext; -import com.facebook.react.util.RNLog; import com.facebook.react.views.text.internal.span.CustomLineHeightSpan; import com.facebook.soloader.SoLoader; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.LinkedList; import java.util.List; import java.util.Objects; @@ -25,19 +19,13 @@ public class MarkdownUtils { SoLoader.loadLibrary("livemarkdown"); } - private static synchronized native String nativeParseMarkdown(String input, int parserId); - public MarkdownUtils(@NonNull ReactContext reactContext) { - mReactContext = reactContext; mAssetManager = reactContext.getAssets(); + mMarkdownParser = new MarkdownParser(reactContext); } - private final @NonNull ReactContext mReactContext; private final @NonNull AssetManager mAssetManager; - - private String mPrevInput; - private String mPrevOutput; - private int mPrevParserId; + private final @NonNull MarkdownParser mMarkdownParser; private MarkdownStyle mMarkdownStyle; private int mParserId; @@ -55,39 +43,8 @@ public void applyMarkdownFormatting(SpannableStringBuilder ssb) { removeSpans(ssb); - String input = ssb.toString(); - String output; - if (input.equals(mPrevInput) && mParserId == mPrevParserId) { - output = mPrevOutput; - } else { - try { - output = nativeParseMarkdown(input, mParserId); - } catch (Exception e) { - output = "[]"; - } - mPrevInput = input; - mPrevOutput = output; - mPrevParserId = mParserId; - } - - List markdownRanges = new LinkedList<>(); - try { - JSONArray ranges = new JSONArray(output); - 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); - int end = start + length; - if (length == 0 || end > input.length()) { - continue; - } - 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()); - } + String text = ssb.toString(); + List markdownRanges = mMarkdownParser.parse(text, mParserId); for (MarkdownRange markdownRange : markdownRanges) { applyRange(ssb, markdownRange); From c822c67046ec423bd18fb8aed04bdf3020faa373 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:44:00 +0000 Subject: [PATCH 08/75] Update package-lock.json version to 0.1.194 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index fa4f787e..e6cf9595 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.193", + "version": "0.1.194", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.193", + "version": "0.1.194", "hasInstallScript": true, "license": "MIT", "workspaces": [ From cc1188c26abf8078cdc665788971ce3c6cba0a86 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:44:00 +0000 Subject: [PATCH 09/75] Update package.json version to 0.1.194 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 96e93120..9af141e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.193", + "version": "0.1.194", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From b59720eaf6c8d2af796c59272d3414550a4bc54b Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Thu, 5 Dec 2024 15:23:19 +0100 Subject: [PATCH 10/75] Move loading shared library to `MarkdownParser` (#562) --- .../main/java/com/expensify/livemarkdown/MarkdownParser.java | 5 +++++ .../main/java/com/expensify/livemarkdown/MarkdownUtils.java | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java index 17bd0e71..e79963bb 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java @@ -4,6 +4,7 @@ import com.facebook.react.bridge.ReactContext; import com.facebook.react.util.RNLog; +import com.facebook.soloader.SoLoader; import org.json.JSONArray; import org.json.JSONException; @@ -14,6 +15,10 @@ import java.util.List; public class MarkdownParser { + static { + SoLoader.loadLibrary("livemarkdown"); + } + private final @NonNull ReactContext mReactContext; private String mPrevText; private int mPrevParserId; diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java index abb213d0..7b840903 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java @@ -9,16 +9,11 @@ import com.expensify.livemarkdown.spans.*; import com.facebook.react.bridge.ReactContext; import com.facebook.react.views.text.internal.span.CustomLineHeightSpan; -import com.facebook.soloader.SoLoader; import java.util.List; import java.util.Objects; public class MarkdownUtils { - static { - SoLoader.loadLibrary("livemarkdown"); - } - public MarkdownUtils(@NonNull ReactContext reactContext) { mAssetManager = reactContext.getAssets(); mMarkdownParser = new MarkdownParser(reactContext); From d9c202d0b2a13d4fd61124dda7e09791c9c93083 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:24:12 +0000 Subject: [PATCH 11/75] Update package-lock.json version to 0.1.195 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index e6cf9595..6df12533 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.194", + "version": "0.1.195", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.194", + "version": "0.1.195", "hasInstallScript": true, "license": "MIT", "workspaces": [ From ee83f2cdb479afbd9ae83f11e7857e853a88109c Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:24:13 +0000 Subject: [PATCH 12/75] Update package.json version to 0.1.195 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9af141e9..d46f8ff4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.194", + "version": "0.1.195", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From 9ada37e611fa1df4a9132d0a21025ad3d226e5ae Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Thu, 5 Dec 2024 16:38:24 +0100 Subject: [PATCH 13/75] Move formatting logic to separate class on Android (#563) --- .../livemarkdown/MarkdownFormatter.java | 116 ++++++++++++++++++ .../expensify/livemarkdown/MarkdownUtils.java | 101 +-------------- 2 files changed, 119 insertions(+), 98 deletions(-) create mode 100644 android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java new file mode 100644 index 00000000..c7f683bf --- /dev/null +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java @@ -0,0 +1,116 @@ +package com.expensify.livemarkdown; + +import android.content.res.AssetManager; +import android.text.SpannableStringBuilder; +import android.text.Spanned; + +import androidx.annotation.NonNull; + +import com.expensify.livemarkdown.spans.*; +import com.facebook.react.views.text.internal.span.CustomLineHeightSpan; + +import java.util.List; +import java.util.Objects; + +public class MarkdownFormatter { + private final @NonNull AssetManager mAssetManager; + + public MarkdownFormatter(@NonNull AssetManager assetManager) { + mAssetManager = assetManager; + } + + public void format(SpannableStringBuilder ssb, List markdownRanges, @NonNull MarkdownStyle markdownStyle) { + Objects.requireNonNull(markdownStyle, "mMarkdownStyle is null"); + removeSpans(ssb); + applyRanges(ssb, markdownRanges, markdownStyle); + } + + 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); + } + } + + private void applyRanges(SpannableStringBuilder ssb, List markdownRanges, @NonNull MarkdownStyle markdownStyle) { + for (MarkdownRange markdownRange : markdownRanges) { + applyRange(ssb, markdownRange, markdownStyle); + } + } + + private void applyRange(SpannableStringBuilder ssb, MarkdownRange markdownRange, MarkdownStyle markdownStyle) { + String type = markdownRange.getType(); + int start = markdownRange.getStart(); + int end = start + markdownRange.getLength(); + switch (type) { + case "bold": + setSpan(ssb, new MarkdownBoldSpan(), start, end); + break; + case "italic": + setSpan(ssb, new MarkdownItalicSpan(), start, end); + break; + case "strikethrough": + setSpan(ssb, new MarkdownStrikethroughSpan(), start, end); + break; + case "emoji": + setSpan(ssb, new MarkdownEmojiSpan(markdownStyle.getEmojiFontSize()), start, end); + break; + case "mention-here": + setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getMentionHereColor()), start, end); + setSpan(ssb, new MarkdownBackgroundColorSpan(markdownStyle.getMentionHereBackgroundColor()), start, end); + break; + case "mention-user": + // TODO: change mention color when it mentions current user + setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getMentionUserColor()), start, end); + setSpan(ssb, new MarkdownBackgroundColorSpan(markdownStyle.getMentionUserBackgroundColor()), start, end); + break; + case "mention-report": + setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getMentionReportColor()), start, end); + setSpan(ssb, new MarkdownBackgroundColorSpan(markdownStyle.getMentionReportBackgroundColor()), start, end); + break; + case "syntax": + setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getSyntaxColor()), start, end); + break; + case "link": + setSpan(ssb, new MarkdownUnderlineSpan(), start, end); + setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getLinkColor()), start, end); + break; + case "code": + setSpan(ssb, new MarkdownFontFamilySpan(markdownStyle.getCodeFontFamily(), mAssetManager), start, end); + setSpan(ssb, new MarkdownFontSizeSpan(markdownStyle.getCodeFontSize()), start, end); + setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getCodeColor()), start, end); + setSpan(ssb, new MarkdownBackgroundColorSpan(markdownStyle.getCodeBackgroundColor()), start, end); + break; + case "pre": + setSpan(ssb, new MarkdownFontFamilySpan(markdownStyle.getPreFontFamily(), mAssetManager), start, end); + setSpan(ssb, new MarkdownFontSizeSpan(markdownStyle.getPreFontSize()), start, end); + setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getPreColor()), start, end); + setSpan(ssb, new MarkdownBackgroundColorSpan(markdownStyle.getPreBackgroundColor()), start, end); + break; + case "h1": + setSpan(ssb, new MarkdownBoldSpan(), start, end); + CustomLineHeightSpan[] spans = ssb.getSpans(0, ssb.length(), CustomLineHeightSpan.class); + if (spans.length >= 1) { + int lineHeight = spans[0].getLineHeight(); + setSpan(ssb, new MarkdownLineHeightSpan(lineHeight * 1.5f), start, end); + } + // NOTE: size span must be set after line height span to avoid height jumps + setSpan(ssb, new MarkdownFontSizeSpan(markdownStyle.getH1FontSize()), start, end); + break; + case "blockquote": + MarkdownBlockquoteSpan span = new MarkdownBlockquoteSpan( + markdownStyle.getBlockquoteBorderColor(), + markdownStyle.getBlockquoteBorderWidth(), + markdownStyle.getBlockquoteMarginLeft(), + markdownStyle.getBlockquotePaddingLeft(), + markdownRange.getDepth()); + setSpan(ssb, span, start, end); + break; + } + } + + private void setSpan(SpannableStringBuilder ssb, 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/MarkdownUtils.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java index 7b840903..be72ec4a 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java @@ -1,26 +1,21 @@ package com.expensify.livemarkdown; -import android.content.res.AssetManager; import android.text.SpannableStringBuilder; -import android.text.Spanned; import androidx.annotation.NonNull; -import com.expensify.livemarkdown.spans.*; import com.facebook.react.bridge.ReactContext; -import com.facebook.react.views.text.internal.span.CustomLineHeightSpan; import java.util.List; -import java.util.Objects; public class MarkdownUtils { public MarkdownUtils(@NonNull ReactContext reactContext) { - mAssetManager = reactContext.getAssets(); mMarkdownParser = new MarkdownParser(reactContext); + mMarkdownFormatter = new MarkdownFormatter(reactContext.getAssets()); } - private final @NonNull AssetManager mAssetManager; private final @NonNull MarkdownParser mMarkdownParser; + private final @NonNull MarkdownFormatter mMarkdownFormatter; private MarkdownStyle mMarkdownStyle; private int mParserId; @@ -34,98 +29,8 @@ public void setParserId(int parserId) { } public void applyMarkdownFormatting(SpannableStringBuilder ssb) { - Objects.requireNonNull(mMarkdownStyle, "mMarkdownStyle is null"); - - removeSpans(ssb); - String text = ssb.toString(); List markdownRanges = mMarkdownParser.parse(text, mParserId); - - for (MarkdownRange markdownRange : markdownRanges) { - applyRange(ssb, markdownRange); - } - } - - private void applyRange(SpannableStringBuilder ssb, MarkdownRange markdownRange) { - String type = markdownRange.getType(); - int start = markdownRange.getStart(); - int end = start + markdownRange.getLength(); - switch (type) { - case "bold": - setSpan(ssb, new MarkdownBoldSpan(), start, end); - break; - case "italic": - setSpan(ssb, new MarkdownItalicSpan(), start, end); - break; - case "strikethrough": - setSpan(ssb, new MarkdownStrikethroughSpan(), start, end); - break; - case "emoji": - setSpan(ssb, new MarkdownEmojiSpan(mMarkdownStyle.getEmojiFontSize()), start, end); - break; - case "mention-here": - setSpan(ssb, new MarkdownForegroundColorSpan(mMarkdownStyle.getMentionHereColor()), start, end); - setSpan(ssb, new MarkdownBackgroundColorSpan(mMarkdownStyle.getMentionHereBackgroundColor()), start, end); - break; - case "mention-user": - // TODO: change mention color when it mentions current user - setSpan(ssb, new MarkdownForegroundColorSpan(mMarkdownStyle.getMentionUserColor()), start, end); - setSpan(ssb, new MarkdownBackgroundColorSpan(mMarkdownStyle.getMentionUserBackgroundColor()), start, end); - break; - case "mention-report": - setSpan(ssb, new MarkdownForegroundColorSpan(mMarkdownStyle.getMentionReportColor()), start, end); - setSpan(ssb, new MarkdownBackgroundColorSpan(mMarkdownStyle.getMentionReportBackgroundColor()), start, end); - break; - case "syntax": - setSpan(ssb, new MarkdownForegroundColorSpan(mMarkdownStyle.getSyntaxColor()), start, end); - break; - case "link": - setSpan(ssb, new MarkdownUnderlineSpan(), start, end); - setSpan(ssb, new MarkdownForegroundColorSpan(mMarkdownStyle.getLinkColor()), start, end); - break; - case "code": - setSpan(ssb, new MarkdownFontFamilySpan(mMarkdownStyle.getCodeFontFamily(), mAssetManager), start, end); - setSpan(ssb, new MarkdownFontSizeSpan(mMarkdownStyle.getCodeFontSize()), start, end); - setSpan(ssb, new MarkdownForegroundColorSpan(mMarkdownStyle.getCodeColor()), start, end); - setSpan(ssb, new MarkdownBackgroundColorSpan(mMarkdownStyle.getCodeBackgroundColor()), start, end); - break; - case "pre": - setSpan(ssb, new MarkdownFontFamilySpan(mMarkdownStyle.getPreFontFamily(), mAssetManager), start, end); - setSpan(ssb, new MarkdownFontSizeSpan(mMarkdownStyle.getPreFontSize()), start, end); - setSpan(ssb, new MarkdownForegroundColorSpan(mMarkdownStyle.getPreColor()), start, end); - setSpan(ssb, new MarkdownBackgroundColorSpan(mMarkdownStyle.getPreBackgroundColor()), start, end); - break; - case "h1": - setSpan(ssb, new MarkdownBoldSpan(), start, end); - CustomLineHeightSpan[] spans = ssb.getSpans(0, ssb.length(), CustomLineHeightSpan.class); - if (spans.length >= 1) { - int lineHeight = spans[0].getLineHeight(); - setSpan(ssb, new MarkdownLineHeightSpan(lineHeight * 1.5f), start, end); - } - // NOTE: size span must be set after line height span to avoid height jumps - setSpan(ssb, new MarkdownFontSizeSpan(mMarkdownStyle.getH1FontSize()), start, end); - break; - case "blockquote": - MarkdownBlockquoteSpan span = new MarkdownBlockquoteSpan( - mMarkdownStyle.getBlockquoteBorderColor(), - mMarkdownStyle.getBlockquoteBorderWidth(), - mMarkdownStyle.getBlockquoteMarginLeft(), - mMarkdownStyle.getBlockquotePaddingLeft(), - markdownRange.getDepth()); - setSpan(ssb, span, start, end); - break; - } - } - - private void setSpan(SpannableStringBuilder ssb, MarkdownSpan span, int start, int end) { - ssb.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - 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); - } + mMarkdownFormatter.format(ssb, markdownRanges, mMarkdownStyle); } } From 6b3732c1697ce420e94c584f7f72b056ae2e0710 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 15:39:22 +0000 Subject: [PATCH 14/75] Update package-lock.json version to 0.1.196 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6df12533..8ad3b40a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.195", + "version": "0.1.196", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.195", + "version": "0.1.196", "hasInstallScript": true, "license": "MIT", "workspaces": [ From 9a7971dcc353cd52ef317afd55527a85ddf1f9d6 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 15:39:22 +0000 Subject: [PATCH 15/75] Update package.json version to 0.1.196 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d46f8ff4..25767da1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.195", + "version": "0.1.196", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From 6a85ff5e539f10d92edbde5ce374c1f21029ec37 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Thu, 5 Dec 2024 17:02:12 +0100 Subject: [PATCH 16/75] Move skipping invalid ranges to parser on iOS (#561) --- apple/MarkdownParser.mm | 4 ++++ apple/RCTMarkdownUtils.mm | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apple/MarkdownParser.mm b/apple/MarkdownParser.mm index c9f3d018..739f106d 100644 --- a/apple/MarkdownParser.mm +++ b/apple/MarkdownParser.mm @@ -46,6 +46,10 @@ @implementation MarkdownParser { const auto &length = static_cast(item.getProperty(rt, "length").asNumber()); const auto &depth = item.hasProperty(rt, "depth") ? static_cast(item.getProperty(rt, "depth").asNumber()) : 1; + if (length == 0 || start + length > text.length) { + continue; + } + NSRange range = NSMakeRange(start, length); MarkdownRange *markdownRange = [[MarkdownRange alloc] initWithType:@(type.c_str()) range:range depth:depth]; [markdownRanges addObject:markdownRange]; diff --git a/apple/RCTMarkdownUtils.mm b/apple/RCTMarkdownUtils.mm index 11a20bca..57d90774 100644 --- a/apple/RCTMarkdownUtils.mm +++ b/apple/RCTMarkdownUtils.mm @@ -70,10 +70,6 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withA } - (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedString type:(const std::string)type range:(NSRange)range depth:(const int)depth { - if (range.length == 0 || range.location + range.length > attributedString.length) { - return; - } - 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") { From b66318918f027774f4ef0445740af9e03437d816 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 16:03:09 +0000 Subject: [PATCH 17/75] Update package-lock.json version to 0.1.197 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8ad3b40a..0756d1d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.196", + "version": "0.1.197", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.196", + "version": "0.1.197", "hasInstallScript": true, "license": "MIT", "workspaces": [ From 01be83f713a1b30fe99d7cadf4e74e58dfba8ddc Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 16:03:10 +0000 Subject: [PATCH 18/75] Update package.json version to 0.1.197 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 25767da1..bf6bb60c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.196", + "version": "0.1.197", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From 5cd661721d5466b4efd2b0abf631b0ba6d74eb0b Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Fri, 6 Dec 2024 09:09:43 +0100 Subject: [PATCH 19/75] 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 3974ba98..4ea7da64 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 e942139d..7b76d438 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 57d90774..8a5ee447 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 d9e49e6f..2a4b70a6 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 20/75] 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 0756d1d2..94d4ecf7 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 21/75] 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 bf6bb60c..b81b9a96 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 22/75] 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 c7f683bf..1fb1469e 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 e79963bb..292c4e13 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 be72ec4a..6877e46f 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 23/75] 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 94d4ecf7..7ea74a30 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 24/75] 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 b81b9a96..38407d38 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 25/75] 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 23b215a2..0e320cd7 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 26/75] 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 7ea74a30..068a83a5 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 27/75] 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 38407d38..bd0743ad 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 28/75] 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 1fb1469e..95760be8 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 292c4e13..3e108db7 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 9c3aedd3..337f1b4a 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 cf691e62..50606f98 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 0e320cd7..d182be1f 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 29/75] 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 068a83a5..e6cdbdbd 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 30/75] 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 bd0743ad..099c9340 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 31/75] 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 95760be8..2ef8ccf9 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 32/75] 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 e6cdbdbd..4f4832dd 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 33/75] 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 099c9340..dd0d82ce 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 34/75] 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 00000000..1f5b1a2e --- /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 00000000..642b487b --- /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 29be3508..9e965e11 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 7b76d438..ea126472 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 8a5ee447..3c90238b 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 2a4b70a6..f3bb3529 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 35/75] 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 4f4832dd..3b9507a7 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 36/75] 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 dd0d82ce..0a705c95 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", From 066dd14bb14720db7971bff088c134b8f9eb3677 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Tue, 10 Dec 2024 11:49:27 +0100 Subject: [PATCH 37/75] Minor improvements in iOS codebase (#575) --- apple/MarkdownCommitHook.mm | 4 ++-- apple/MarkdownFormatter.h | 2 +- apple/MarkdownFormatter.mm | 4 ++-- apple/MarkdownParser.h | 3 ++- apple/MarkdownParser.mm | 4 +++- apple/RCTBackedTextFieldDelegateAdapter+Markdown.mm | 2 +- apple/RCTBaseTextInputView+Markdown.mm | 4 ++-- apple/RCTMarkdownUtils.h | 3 ++- apple/RCTMarkdownUtils.mm | 11 ++++++----- apple/RCTTextInputComponentView+Markdown.mm | 4 ++-- apple/RCTUITextView+Markdown.mm | 2 +- example/ios/Podfile.lock | 8 ++++---- 12 files changed, 28 insertions(+), 23 deletions(-) diff --git a/apple/MarkdownCommitHook.mm b/apple/MarkdownCommitHook.mm index f98f1219..9ff5be22 100644 --- a/apple/MarkdownCommitHook.mm +++ b/apple/MarkdownCommitHook.mm @@ -198,7 +198,7 @@ // apply markdown auto newString = [usedUtils parseMarkdown:nsAttributedString - withAttributes:defaultNSTextAttributes]; + withDefaultTextAttributes:defaultNSTextAttributes]; // create a clone of the old TextInputState and update the // attributed string box to point to the string with markdown @@ -247,7 +247,7 @@ // apply markdown auto newString = [usedUtils parseMarkdown:nsAttributedString - withAttributes:defaultNSTextAttributes]; + withDefaultTextAttributes:defaultNSTextAttributes]; // create a clone of the old TextInputState and update the // attributed string box to point to the string with markdown diff --git a/apple/MarkdownFormatter.h b/apple/MarkdownFormatter.h index 1f5b1a2e..1cf86c85 100644 --- a/apple/MarkdownFormatter.h +++ b/apple/MarkdownFormatter.h @@ -9,7 +9,7 @@ const NSAttributedStringKey RCTLiveMarkdownBlockquoteDepthAttributeName = @"RCTL @interface MarkdownFormatter : NSObject - (nonnull NSAttributedString *)format:(nonnull NSString *)text - withAttributes:(nullable NSDictionary*)attributes + withDefaultTextAttributes:(nonnull NSDictionary *)defaultTextAttributes withMarkdownRanges:(nonnull NSArray *)markdownRanges withMarkdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle; diff --git a/apple/MarkdownFormatter.mm b/apple/MarkdownFormatter.mm index 642b487b..b84200c9 100644 --- a/apple/MarkdownFormatter.mm +++ b/apple/MarkdownFormatter.mm @@ -4,11 +4,11 @@ @implementation MarkdownFormatter - (nonnull NSAttributedString *)format:(nonnull NSString *)text - withAttributes:(nullable NSDictionary *)attributes + withDefaultTextAttributes:(nonnull NSDictionary *)defaultTextAttributes withMarkdownRanges:(nonnull NSArray *)markdownRanges withMarkdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle { - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:text attributes:attributes]; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:text attributes:defaultTextAttributes]; [attributedString beginEditing]; diff --git a/apple/MarkdownParser.h b/apple/MarkdownParser.h index 7ec8d195..407e49b0 100644 --- a/apple/MarkdownParser.h +++ b/apple/MarkdownParser.h @@ -5,7 +5,8 @@ NS_ASSUME_NONNULL_BEGIN @interface MarkdownParser : NSObject -- (NSArray *)parse:(NSString *)text withParserId:(NSNumber *)parserId; +- (NSArray *)parse:(nonnull NSString *)text + withParserId:(nonnull NSNumber *)parserId; NS_ASSUME_NONNULL_END diff --git a/apple/MarkdownParser.mm b/apple/MarkdownParser.mm index 739f106d..4e96050a 100644 --- a/apple/MarkdownParser.mm +++ b/apple/MarkdownParser.mm @@ -9,7 +9,9 @@ @implementation MarkdownParser { NSArray *_prevMarkdownRanges; } -- (NSArray *)parse:(NSString *)text withParserId:(nonnull NSNumber *)parserId { +- (NSArray *)parse:(nonnull NSString *)text + withParserId:(nonnull NSNumber *)parserId +{ @synchronized (self) { if ([text isEqualToString:_prevText] && [parserId isEqualToNumber:_prevParserId]) { return _prevMarkdownRanges; diff --git a/apple/RCTBackedTextFieldDelegateAdapter+Markdown.mm b/apple/RCTBackedTextFieldDelegateAdapter+Markdown.mm index 11c3baf8..d4d8f821 100644 --- a/apple/RCTBackedTextFieldDelegateAdapter+Markdown.mm +++ b/apple/RCTBackedTextFieldDelegateAdapter+Markdown.mm @@ -19,7 +19,7 @@ - (void)markdown_textFieldDidChange if (markdownUtils != nil) { RCTUITextField *backedTextInputView = [self valueForKey:@"_backedTextInputView"]; UITextRange *range = backedTextInputView.selectedTextRange; - backedTextInputView.attributedText = [markdownUtils parseMarkdown:backedTextInputView.attributedText withAttributes:backedTextInputView.defaultTextAttributes]; + backedTextInputView.attributedText = [markdownUtils parseMarkdown:backedTextInputView.attributedText withDefaultTextAttributes:backedTextInputView.defaultTextAttributes]; [backedTextInputView setSelectedTextRange:range notifyDelegate:YES]; } diff --git a/apple/RCTBaseTextInputView+Markdown.mm b/apple/RCTBaseTextInputView+Markdown.mm index 7662d545..ec420068 100644 --- a/apple/RCTBaseTextInputView+Markdown.mm +++ b/apple/RCTBaseTextInputView+Markdown.mm @@ -16,7 +16,7 @@ - (void)markdown_setAttributedText:(NSAttributedString *)attributedText { RCTMarkdownUtils *markdownUtils = [self getMarkdownUtils]; if (markdownUtils != nil) { - attributedText = [markdownUtils parseMarkdown:attributedText withAttributes:self.backedTextInputView.defaultTextAttributes]; + attributedText = [markdownUtils parseMarkdown:attributedText withDefaultTextAttributes:self.backedTextInputView.defaultTextAttributes]; } // Call the original method @@ -46,7 +46,7 @@ - (void)markdown_updateLocalData if (markdownUtils != nil) { id backedTextInputView = self.backedTextInputView; NSAttributedString *oldAttributedText = backedTextInputView.attributedText; - NSAttributedString *newAttributedText = [markdownUtils parseMarkdown:oldAttributedText withAttributes:backedTextInputView.defaultTextAttributes]; + NSAttributedString *newAttributedText = [markdownUtils parseMarkdown:oldAttributedText withDefaultTextAttributes:backedTextInputView.defaultTextAttributes]; UITextRange *range = backedTextInputView.selectedTextRange; // update attributed text without emitting onSelectionChange event diff --git a/apple/RCTMarkdownUtils.h b/apple/RCTMarkdownUtils.h index ea126472..fed14596 100644 --- a/apple/RCTMarkdownUtils.h +++ b/apple/RCTMarkdownUtils.h @@ -8,7 +8,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) RCTMarkdownStyle *markdownStyle; @property (nonatomic) NSNumber *parserId; -- (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withAttributes:(nullable NSDictionary*)attributes; +- (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input + withDefaultTextAttributes:(nonnull NSDictionary *)defaultTextAttributes; @end diff --git a/apple/RCTMarkdownUtils.mm b/apple/RCTMarkdownUtils.mm index 3c90238b..a932e572 100644 --- a/apple/RCTMarkdownUtils.mm +++ b/apple/RCTMarkdownUtils.mm @@ -7,7 +7,7 @@ @implementation RCTMarkdownUtils { MarkdownFormatter *_markdownFormatter; NSString *_prevInputString; NSAttributedString *_prevAttributedString; - NSDictionary *_prevTextAttributes; + NSDictionary *_prevDefaultTextAttributes; __weak RCTMarkdownStyle *_prevMarkdownStyle; __weak NSNumber *_prevParserId; } @@ -22,7 +22,8 @@ - (instancetype)init return self; } -- (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withAttributes:(nullable NSDictionary *)attributes +- (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input + withDefaultTextAttributes:(nonnull NSDictionary *)defaultTextAttributes { @synchronized (self) { if (input == nil) { @@ -30,19 +31,19 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withA } NSString *inputString = [input string]; - if ([inputString isEqualToString:_prevInputString] && [attributes isEqualToDictionary:_prevTextAttributes] && [_markdownStyle isEqual:_prevMarkdownStyle] && [_parserId isEqualToNumber:_prevParserId]) { + if ([inputString isEqualToString:_prevInputString] && [defaultTextAttributes isEqualToDictionary:_prevDefaultTextAttributes] && [_markdownStyle isEqual:_prevMarkdownStyle] && [_parserId isEqualToNumber:_prevParserId]) { return _prevAttributedString; } NSArray *markdownRanges = [_markdownParser parse:inputString withParserId:_parserId]; NSAttributedString *attributedString = [_markdownFormatter format:inputString - withAttributes:attributes + withDefaultTextAttributes:defaultTextAttributes withMarkdownRanges:markdownRanges withMarkdownStyle:_markdownStyle]; _prevInputString = inputString; _prevAttributedString = attributedString; - _prevTextAttributes = attributes; + _prevDefaultTextAttributes = defaultTextAttributes; _prevMarkdownStyle = _markdownStyle; _prevParserId = _parserId; diff --git a/apple/RCTTextInputComponentView+Markdown.mm b/apple/RCTTextInputComponentView+Markdown.mm index 5ce1e63e..6570097c 100644 --- a/apple/RCTTextInputComponentView+Markdown.mm +++ b/apple/RCTTextInputComponentView+Markdown.mm @@ -18,7 +18,7 @@ - (void)setMarkdownUtils:(RCTMarkdownUtils *)markdownUtils { if (markdownUtils != nil) { // force Markdown formatting on first render because `_setAttributedText` is called before `setMarkdownUtils` RCTUITextField *backedTextInputView = [self getBackedTextInputView]; - backedTextInputView.attributedText = [markdownUtils parseMarkdown:backedTextInputView.attributedText withAttributes:backedTextInputView.defaultTextAttributes]; + backedTextInputView.attributedText = [markdownUtils parseMarkdown:backedTextInputView.attributedText withDefaultTextAttributes:backedTextInputView.defaultTextAttributes]; } } @@ -36,7 +36,7 @@ - (void)markdown__setAttributedString:(NSAttributedString *)attributedString RCTMarkdownUtils *markdownUtils = [self getMarkdownUtils]; RCTUITextField *backedTextInputView = [self getBackedTextInputView]; if (markdownUtils != nil && backedTextInputView != nil) { - attributedString = [markdownUtils parseMarkdown:attributedString withAttributes:backedTextInputView.defaultTextAttributes]; + attributedString = [markdownUtils parseMarkdown:attributedString withDefaultTextAttributes:backedTextInputView.defaultTextAttributes]; } else { // If markdownUtils is undefined, the text input hasn't been mounted yet. It will // update its state with the unformatted attributed string, we want to prevent displaying diff --git a/apple/RCTUITextView+Markdown.mm b/apple/RCTUITextView+Markdown.mm index 70f2d882..5a49abe9 100644 --- a/apple/RCTUITextView+Markdown.mm +++ b/apple/RCTUITextView+Markdown.mm @@ -17,7 +17,7 @@ - (void)markdown_textDidChange RCTMarkdownUtils *markdownUtils = [self getMarkdownUtils]; if (markdownUtils != nil) { UITextRange *range = self.selectedTextRange; - super.attributedText = [markdownUtils parseMarkdown:self.attributedText withAttributes:self.defaultTextAttributes]; + super.attributedText = [markdownUtils parseMarkdown:self.attributedText withDefaultTextAttributes:self.defaultTextAttributes]; [super setSelectedTextRange:range]; // prevents cursor from jumping at the end when typing in the middle of the text self.typingAttributes = self.defaultTextAttributes; // removes indent in new line when typing after blockquote } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index f3bb3529..aba22425 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.199): + - RNLiveMarkdown (0.1.203): - DoubleConversion - glog - hermes-engine @@ -1517,10 +1517,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/newarch (= 0.1.199) + - RNLiveMarkdown/newarch (= 0.1.203) - RNReanimated/worklets - Yoga - - RNLiveMarkdown/newarch (0.1.199): + - RNLiveMarkdown/newarch (0.1.203): - DoubleConversion - glog - hermes-engine @@ -1897,7 +1897,7 @@ SPEC CHECKSUMS: React-utils: f2afa6acd905ca2ce7bb8ffb4a22f7f8a12534e8 ReactCodegen: e35c23cdd36922f6d2990c6c1f1b022ade7ad74d ReactCommon: 289214026502e6a93484f4a46bcc0efa4f3f2864 - RNLiveMarkdown: 18dd4ceada29d66a6b7c29b1b0df589e2fc82183 + RNLiveMarkdown: ed779eaf35a346f5254079b825a13f0a032ba64a RNReanimated: ab6c33a61e90c4cbe5dbcbe65bd6c7cb3be167e6 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: 1354c027ab07c7736f99a3bef16172d6f1b12b47 From 23b01eb5f7e824fdb1e7fc814b9e6b7226846f36 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:50:16 +0000 Subject: [PATCH 38/75] Update package-lock.json version to 0.1.204 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3b9507a7..8cf7d34f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.203", + "version": "0.1.204", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.203", + "version": "0.1.204", "hasInstallScript": true, "license": "MIT", "workspaces": [ From 440b75c1416aff7491da6ce4707d277f4514d142 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:50:17 +0000 Subject: [PATCH 39/75] Update package.json version to 0.1.204 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0a705c95..49a4f64b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.203", + "version": "0.1.204", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From 0a5d4f08ea8f34c6cb4cbd2ca2863d4639a8cc55 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Tue, 10 Dec 2024 14:54:48 +0100 Subject: [PATCH 40/75] Throw error when `html-entities` is not workletized (#576) --- src/parseExpensiMark.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/parseExpensiMark.ts b/src/parseExpensiMark.ts index 4162eb6c..673734af 100644 --- a/src/parseExpensiMark.ts +++ b/src/parseExpensiMark.ts @@ -1,9 +1,27 @@ 'worklet'; +import {Platform} from 'react-native'; import {ExpensiMark} from 'expensify-common'; import {unescapeText} from 'expensify-common/dist/utils'; +import {decode} from 'html-entities'; +import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/commonTypes'; import type {MarkdownType, MarkdownRange} from './commonTypes'; +function isWeb() { + return Platform.OS === 'web'; +} + +function isJest() { + return !!global.process.env.JEST_WORKER_ID; +} + +// eslint-disable-next-line no-underscore-dangle +if (__DEV__ && !isWeb() && !isJest() && (decode as WorkletFunction).__workletHash === undefined) { + throw new Error( + "[react-native-live-markdown] `parseExpensiMark` requires `html-entities` package to be workletized. Please add `'worklet';` directive at the top of `node_modules/html-entities/lib/index.js` using patch-package.", + ); +} + const MAX_PARSABLE_LENGTH = 4000; type Token = ['TEXT' | 'HTML', string]; From 5eb2f0951a6ed7c2cef253388f18cd6ef5470a08 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:55:44 +0000 Subject: [PATCH 41/75] Update package-lock.json version to 0.1.205 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8cf7d34f..33058145 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.204", + "version": "0.1.205", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.204", + "version": "0.1.205", "hasInstallScript": true, "license": "MIT", "workspaces": [ From 540db5fd3103d8239a8e0a0b8d8bcb71e28913eb Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:55:45 +0000 Subject: [PATCH 42/75] Update package.json version to 0.1.205 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 49a4f64b..9d3243de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.204", + "version": "0.1.205", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From 808fca1183166021b9544722d3992f0df7079756 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Wed, 11 Dec 2024 09:30:43 +0100 Subject: [PATCH 43/75] Bump react-native-reanimated to 3.16.4 (#577) --- README.md | 2 +- example/ios/Podfile.lock | 24 ++++++++++++------------ example/package.json | 2 +- package-lock.json | 12 ++++++------ package.json | 4 ++-- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index d8c2dec5..99027bd8 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ npm install @expensify/react-native-live-markdown react-native-reanimated expens npx expo install @expensify/react-native-live-markdown react-native-reanimated expensify-common ``` -React Native Live Markdown requires [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated) 3.16.3 or newer and [expensify-common](https://github.com/Expensify/expensify-common) 2.0.108 or newer. +React Native Live Markdown requires [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated) 3.16.4 or newer and [expensify-common](https://github.com/Expensify/expensify-common) 2.0.108 or newer. Then, install the iOS dependencies with CocoaPods: diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index aba22425..7aed26be 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.203): + - RNLiveMarkdown (0.1.205): - DoubleConversion - glog - hermes-engine @@ -1517,10 +1517,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/newarch (= 0.1.203) + - RNLiveMarkdown/newarch (= 0.1.205) - RNReanimated/worklets - Yoga - - RNLiveMarkdown/newarch (0.1.203): + - RNLiveMarkdown/newarch (0.1.205): - DoubleConversion - glog - hermes-engine @@ -1542,7 +1542,7 @@ PODS: - ReactCommon/turbomodule/core - RNReanimated/worklets - Yoga - - RNReanimated (3.16.3): + - RNReanimated (3.16.4): - DoubleConversion - glog - hermes-engine @@ -1562,10 +1562,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated (= 3.16.3) - - RNReanimated/worklets (= 3.16.3) + - RNReanimated/reanimated (= 3.16.4) + - RNReanimated/worklets (= 3.16.4) - Yoga - - RNReanimated/reanimated (3.16.3): + - RNReanimated/reanimated (3.16.4): - DoubleConversion - glog - hermes-engine @@ -1585,9 +1585,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated/apple (= 3.16.3) + - RNReanimated/reanimated/apple (= 3.16.4) - Yoga - - RNReanimated/reanimated/apple (3.16.3): + - RNReanimated/reanimated/apple (3.16.4): - DoubleConversion - glog - hermes-engine @@ -1608,7 +1608,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNReanimated/worklets (3.16.3): + - RNReanimated/worklets (3.16.4): - DoubleConversion - glog - hermes-engine @@ -1897,8 +1897,8 @@ SPEC CHECKSUMS: React-utils: f2afa6acd905ca2ce7bb8ffb4a22f7f8a12534e8 ReactCodegen: e35c23cdd36922f6d2990c6c1f1b022ade7ad74d ReactCommon: 289214026502e6a93484f4a46bcc0efa4f3f2864 - RNLiveMarkdown: ed779eaf35a346f5254079b825a13f0a032ba64a - RNReanimated: ab6c33a61e90c4cbe5dbcbe65bd6c7cb3be167e6 + RNLiveMarkdown: bf0f16b1e8c3320d600a5931d270e19afef9fa92 + RNReanimated: 75df06d3a81fc147b83056ae469512f573365b1d SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: 1354c027ab07c7736f99a3bef16172d6f1b12b47 diff --git a/example/package.json b/example/package.json index c75e8e09..0bf17b18 100644 --- a/example/package.json +++ b/example/package.json @@ -12,7 +12,7 @@ "expensify-common": "2.0.108", "react": "18.3.1", "react-native": "0.75.3", - "react-native-reanimated": "3.16.3" + "react-native-reanimated": "3.16.4" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/package-lock.json b/package-lock.json index 33058145..5ec48bcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,7 @@ "react": "18.3.1", "react-native": "0.75.3", "react-native-builder-bob": "^0.20.0", - "react-native-reanimated": "3.16.3", + "react-native-reanimated": "3.16.4", "react-native-web": "^0.19.10", "release-it": "^15.0.0", "turbo": "^1.10.7", @@ -61,7 +61,7 @@ "expensify-common": ">=2.0.108", "react": "*", "react-native": "*", - "react-native-reanimated": ">=3.16.3" + "react-native-reanimated": ">=3.16.4" } }, "example": { @@ -71,7 +71,7 @@ "expensify-common": "2.0.108", "react": "18.3.1", "react-native": "0.75.3", - "react-native-reanimated": "3.16.3" + "react-native-reanimated": "3.16.4" }, "devDependencies": { "@babel/core": "^7.20.0", @@ -24615,9 +24615,9 @@ } }, "node_modules/react-native-reanimated": { - "version": "3.16.3", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.16.3.tgz", - "integrity": "sha512-OWlA6e1oHhytTpc7WiSZ7Tmb8OYwLKYZz29Sz6d6WAg60Hm5GuAiKIWUG7Ako7FLcYhFkA0pEQ2xPMEYUo9vlw==", + "version": "3.16.4", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.16.4.tgz", + "integrity": "sha512-dF1Vvu8gG+p0+DmBhKMTx5X9iw/rH1ZF9WaIn2nW0c5rxsVFf00axmDgaAdPxNWblmtLnroaKwrV7SjMUyOx+g==", "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", diff --git a/package.json b/package.json index 9d3243de..2fbe4438 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "react": "18.3.1", "react-native": "0.75.3", "react-native-builder-bob": "^0.20.0", - "react-native-reanimated": "3.16.3", + "react-native-reanimated": "3.16.4", "react-native-web": "^0.19.10", "release-it": "^15.0.0", "turbo": "^1.10.7", @@ -103,7 +103,7 @@ "expensify-common": ">=2.0.108", "react": "*", "react-native": "*", - "react-native-reanimated": ">=3.16.3" + "react-native-reanimated": ">=3.16.4" }, "workspaces": [ "./example", From 5e07c26bd525dc87ec81e10c6b80c3fc44ebee8e Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 08:31:35 +0000 Subject: [PATCH 44/75] Update package-lock.json version to 0.1.206 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5ec48bcd..4c4d0467 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.205", + "version": "0.1.206", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.205", + "version": "0.1.206", "hasInstallScript": true, "license": "MIT", "workspaces": [ From 81601a966a0d2254264bda2cef2de18b35eb5a69 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 08:31:36 +0000 Subject: [PATCH 45/75] Update package.json version to 0.1.206 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2fbe4438..00f6f7ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.205", + "version": "0.1.206", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From 67724f0e79a7ad14dc3f58677f00a064d89859aa Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Wed, 11 Dec 2024 09:51:00 +0100 Subject: [PATCH 46/75] add mock (#578) --- mock/index.ts | 14 ++++++++++++++ package.json | 1 + src/MarkdownTextInput.tsx | 5 ++--- tsconfig.json | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 mock/index.ts diff --git a/mock/index.ts b/mock/index.ts new file mode 100644 index 00000000..6e082dea --- /dev/null +++ b/mock/index.ts @@ -0,0 +1,14 @@ +import {MarkdownTextInput} from '../src'; +import type {parseExpensiMark} from '../src'; + +global.jsi_setMarkdownRuntime = jest.fn(); +global.jsi_registerMarkdownWorklet = jest.fn(); +global.jsi_unregisterMarkdownWorklet = jest.fn(); + +const parseExpensiMarkMock: typeof parseExpensiMark = () => { + 'worklet'; + + return []; +}; + +export {MarkdownTextInput, parseExpensiMarkMock as parseExpensiMark}; diff --git a/package.json b/package.json index 00f6f7ce..9ed7dd4a 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "files": [ "src", "lib", + "mock", "android", "apple", "cpp", diff --git a/src/MarkdownTextInput.tsx b/src/MarkdownTextInput.tsx index c4ec2467..b744a9bd 100644 --- a/src/MarkdownTextInput.tsx +++ b/src/MarkdownTextInput.tsx @@ -28,10 +28,9 @@ function initializeLiveMarkdownIfNeeded() { if (initialized) { return; } - if (!NativeLiveMarkdownModule) { - throw new Error('[react-native-live-markdown] NativeLiveMarkdownModule is not available'); + if (NativeLiveMarkdownModule) { + NativeLiveMarkdownModule.install(); } - NativeLiveMarkdownModule.install(); if (!global.jsi_setMarkdownRuntime) { throw new Error('[react-native-live-markdown] global.jsi_setMarkdownRuntime is not available'); } diff --git a/tsconfig.json b/tsconfig.json index 5af82eae..a92fd85d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,6 @@ "verbatimModuleSyntax": true, "typeRoots": ["node_modules/@types"] }, - "include": ["src/**/*"], + "include": ["src/**/*", "mock/**/*"], "exclude": ["**/node_modules/**/*", "**/lib/**/*", "example/src/**/*", "WebExample/**/*"] } From 2538fb6ae82953f8ff22d2deb842b2bff7f45b19 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 08:51:52 +0000 Subject: [PATCH 47/75] Update package-lock.json version to 0.1.207 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4c4d0467..ae9e17c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.206", + "version": "0.1.207", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.206", + "version": "0.1.207", "hasInstallScript": true, "license": "MIT", "workspaces": [ From 2581f3a82ed4f04b5c8860cfb5056f1388fe5d18 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 08:51:53 +0000 Subject: [PATCH 48/75] Update package.json version to 0.1.207 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9ed7dd4a..4dc6b088 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.206", + "version": "0.1.207", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From 8b6414eb4cdb3e04a33cd1d3c6e41411a5669f43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= <39538890+Skalakid@users.noreply.github.com> Date: Thu, 12 Dec 2024 00:39:00 -0800 Subject: [PATCH 49/75] Fix codeblock text parsing (#581) --- src/parseExpensiMark.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parseExpensiMark.ts b/src/parseExpensiMark.ts index 673734af..cbf69845 100644 --- a/src/parseExpensiMark.ts +++ b/src/parseExpensiMark.ts @@ -172,7 +172,7 @@ function parseTreeToTextAndRanges(tree: StackItem): [string, MarkdownRange[]] { text += '\n'; } else if (node.tag.startsWith(' Date: Thu, 12 Dec 2024 08:39:52 +0000 Subject: [PATCH 50/75] Update package-lock.json version to 0.1.208 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index ae9e17c9..8340261a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.207", + "version": "0.1.208", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.207", + "version": "0.1.208", "hasInstallScript": true, "license": "MIT", "workspaces": [ From 4976b59757d2b4eda21985c6cd5f53abe8e740df Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:39:53 +0000 Subject: [PATCH 51/75] Update package.json version to 0.1.208 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4dc6b088..daed2b23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.207", + "version": "0.1.208", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From e1ccea1d08b99b073439818b29d34a6746257c14 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Tue, 17 Dec 2024 21:52:21 +0100 Subject: [PATCH 52/75] Fix build with `USE_FRAMEWORKS=static` (#589) --- RNLiveMarkdown.podspec | 11 ++++++++++- example/ios/Podfile.lock | 8 ++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/RNLiveMarkdown.podspec b/RNLiveMarkdown.podspec index de9baafc..34e9fd00 100644 --- a/RNLiveMarkdown.podspec +++ b/RNLiveMarkdown.podspec @@ -4,6 +4,10 @@ react_native_node_modules_dir = ENV['REACT_NATIVE_NODE_MODULES_DIR'] || File.joi react_native_json = JSON.parse(File.read(File.join(react_native_node_modules_dir, 'react-native/package.json'))) react_native_minor_version = react_native_json['version'].split('.')[1].to_i +pods_root = Pod::Config.instance.project_pods_root +react_native_reanimated_node_modules_dir = ENV['REACT_NATIVE_REANIMATED_NODE_MODULES_DIR'] || File.dirname(`cd "#{Pod::Config.instance.installation_root.to_s}" && node --print "require.resolve('react-native-reanimated/package.json')"`) +react_native_reanimated_node_modules_dir_from_pods_root = Pathname.new(react_native_reanimated_node_modules_dir).relative_path_from(pods_root).to_s + package = JSON.parse(File.read(File.join(__dir__, "package.json"))) folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' @@ -23,7 +27,11 @@ Pod::Spec.new do |s| s.dependency "RNReanimated/worklets" s.xcconfig = { - "OTHER_CFLAGS" => "$(inherited) -DREACT_NATIVE_MINOR_VERSION=#{react_native_minor_version}" + "OTHER_CFLAGS" => "$(inherited) -DREACT_NATIVE_MINOR_VERSION=#{react_native_minor_version}", + "HEADER_SEARCH_PATHS" => [ + "\"$(PODS_ROOT)/#{react_native_reanimated_node_modules_dir_from_pods_root}/apple\"", + "\"$(PODS_ROOT)/#{react_native_reanimated_node_modules_dir_from_pods_root}/Common/cpp\"", + ].join(' '), } install_modules_dependencies(s) @@ -33,6 +41,7 @@ Pod::Spec.new do |s| "react/renderer/textlayoutmanager/platform/ios", "react/renderer/components/textinput/platform/ios", ]) + add_dependency(s, "React-rendererconsistency") end if ENV['RCT_NEW_ARCH_ENABLED'] == '1' diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 7aed26be..5a43e11a 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.205): + - RNLiveMarkdown (0.1.208): - DoubleConversion - glog - hermes-engine @@ -1517,10 +1517,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/newarch (= 0.1.205) + - RNLiveMarkdown/newarch (= 0.1.208) - RNReanimated/worklets - Yoga - - RNLiveMarkdown/newarch (0.1.205): + - RNLiveMarkdown/newarch (0.1.208): - DoubleConversion - glog - hermes-engine @@ -1897,7 +1897,7 @@ SPEC CHECKSUMS: React-utils: f2afa6acd905ca2ce7bb8ffb4a22f7f8a12534e8 ReactCodegen: e35c23cdd36922f6d2990c6c1f1b022ade7ad74d ReactCommon: 289214026502e6a93484f4a46bcc0efa4f3f2864 - RNLiveMarkdown: bf0f16b1e8c3320d600a5931d270e19afef9fa92 + RNLiveMarkdown: 1ee098c3a830c3133c23fc163b0aff29398a293e RNReanimated: 75df06d3a81fc147b83056ae469512f573365b1d SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: 1354c027ab07c7736f99a3bef16172d6f1b12b47 From 31f36be8731cb5b71c020b2017a04957981e4b37 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 20:53:14 +0000 Subject: [PATCH 53/75] Update package-lock.json version to 0.1.209 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8340261a..d3d61e4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.208", + "version": "0.1.209", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.208", + "version": "0.1.209", "hasInstallScript": true, "license": "MIT", "workspaces": [ From bc6ef7608e1ea7ba288e645a71bb3f4f060657d6 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 20:53:15 +0000 Subject: [PATCH 54/75] Update package.json version to 0.1.209 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index daed2b23..46b0402e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.208", + "version": "0.1.209", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From 8b1789a13e2cbb48ec7b0abe9efc7bc9f2dc57dc Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader <56457735+ikevin127@users.noreply.github.com> Date: Wed, 18 Dec 2024 04:44:41 -0800 Subject: [PATCH 55/75] FIX: Android - Live markdown style not applied consistently (#590) --- .../expensify/livemarkdown/MarkdownTextInputDecoratorView.java | 1 + 1 file changed, 1 insertion(+) diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorView.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorView.java index 6bd1a68f..b7ab2aa0 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorView.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorView.java @@ -61,6 +61,7 @@ protected void onAttachedToWindow() { mReactEditText = (ReactEditText) previousSibling; mTextWatcher = new MarkdownTextWatcher(mMarkdownUtils); mReactEditText.addTextChangedListener(mTextWatcher); + applyNewStyles(); } } From b8c6ff393582569c66bd4ec925d21849da3aa103 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 12:45:37 +0000 Subject: [PATCH 56/75] Update package-lock.json version to 0.1.210 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index d3d61e4d..f313c827 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.209", + "version": "0.1.210", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.209", + "version": "0.1.210", "hasInstallScript": true, "license": "MIT", "workspaces": [ From f165434a62852973f1e46b6271a49c8928d73cbe Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 12:45:38 +0000 Subject: [PATCH 57/75] Update package.json version to 0.1.210 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 46b0402e..17a124ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.209", + "version": "0.1.210", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From a068201769058888e9d87cbd453101bf4d3afe03 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Sat, 21 Dec 2024 20:19:06 +0100 Subject: [PATCH 58/75] Fix blockquote line height on iOS (#587) --- apple/MarkdownFormatter.mm | 24 ++++++++++++++++-------- example/ios/Podfile.lock | 8 ++++---- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/apple/MarkdownFormatter.mm b/apple/MarkdownFormatter.mm index b84200c9..fdb4116f 100644 --- a/apple/MarkdownFormatter.mm +++ b/apple/MarkdownFormatter.mm @@ -24,10 +24,15 @@ - (nonnull NSAttributedString *)format:(nonnull NSString *)text type:std::string([markdownRange.type UTF8String]) range:markdownRange.range depth:markdownRange.depth - markdownStyle:markdownStyle]; + markdownStyle:markdownStyle + defaultTextAttributes:defaultTextAttributes]; } - RCTApplyBaselineOffset(attributedString); + [attributedString.string enumerateSubstringsInRange:NSMakeRange(0, attributedString.length) + options:NSStringEnumerationByLines | NSStringEnumerationSubstringNotRequired + usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) { + RCTApplyBaselineOffset(attributedString, enclosingRange); + }]; [attributedString endEditing]; @@ -38,7 +43,9 @@ - (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedStri type:(const std::string)type range:(const NSRange)range depth:(const int)depth - markdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle { + markdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle + defaultTextAttributes:(nonnull NSDictionary *)defaultTextAttributes +{ 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") { @@ -99,7 +106,8 @@ - (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedStri [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]; + NSParagraphStyle *defaultParagraphStyle = defaultTextAttributes[NSParagraphStyleAttributeName]; + NSMutableParagraphStyle *paragraphStyle = defaultParagraphStyle != nil ? [defaultParagraphStyle mutableCopy] : [NSMutableParagraphStyle new]; paragraphStyle.firstLineHeadIndent = indent; paragraphStyle.headIndent = indent; [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; @@ -112,12 +120,12 @@ - (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedStri } } -static void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText) +static void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText, NSRange attributedTextRange) { __block CGFloat maximumLineHeight = 0; [attributedText enumerateAttribute:NSParagraphStyleAttributeName - inRange:NSMakeRange(0, attributedText.length) + inRange:attributedTextRange options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(NSParagraphStyle *paragraphStyle, __unused NSRange range, __unused BOOL *stop) { if (!paragraphStyle) { @@ -135,7 +143,7 @@ static void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText) __block CGFloat maximumFontLineHeight = 0; [attributedText enumerateAttribute:NSFontAttributeName - inRange:NSMakeRange(0, attributedText.length) + inRange:attributedTextRange options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(UIFont *font, NSRange range, __unused BOOL *stop) { if (!font) { @@ -152,7 +160,7 @@ static void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText) CGFloat baseLineOffset = (maximumLineHeight - maximumFontLineHeight) / 2.0; [attributedText addAttribute:NSBaselineOffsetAttributeName value:@(baseLineOffset) - range:NSMakeRange(0, attributedText.length)]; + range:attributedTextRange]; } @end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 5a43e11a..a8d14f2a 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.208): + - RNLiveMarkdown (0.1.210): - DoubleConversion - glog - hermes-engine @@ -1517,10 +1517,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/newarch (= 0.1.208) + - RNLiveMarkdown/newarch (= 0.1.210) - RNReanimated/worklets - Yoga - - RNLiveMarkdown/newarch (0.1.208): + - RNLiveMarkdown/newarch (0.1.210): - DoubleConversion - glog - hermes-engine @@ -1897,7 +1897,7 @@ SPEC CHECKSUMS: React-utils: f2afa6acd905ca2ce7bb8ffb4a22f7f8a12534e8 ReactCodegen: e35c23cdd36922f6d2990c6c1f1b022ade7ad74d ReactCommon: 289214026502e6a93484f4a46bcc0efa4f3f2864 - RNLiveMarkdown: 1ee098c3a830c3133c23fc163b0aff29398a293e + RNLiveMarkdown: 687bc45ffb3b4af261f414fea169f10eae5ac261 RNReanimated: 75df06d3a81fc147b83056ae469512f573365b1d SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: 1354c027ab07c7736f99a3bef16172d6f1b12b47 From 8fbfa803deb69f5adbe518b1b975982712832f60 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Sat, 21 Dec 2024 19:19:55 +0000 Subject: [PATCH 59/75] Update package-lock.json version to 0.1.211 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f313c827..dee2f10c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.210", + "version": "0.1.211", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.210", + "version": "0.1.211", "hasInstallScript": true, "license": "MIT", "workspaces": [ From 6447ea9ec54830f1d55c1eac0e9274ce93a4c769 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Sat, 21 Dec 2024 19:19:56 +0000 Subject: [PATCH 60/75] Update package.json version to 0.1.211 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 17a124ae..9e554fc2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.210", + "version": "0.1.211", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From 7b34f62526d989bcc0ad7398dffd18cda0c3c12c Mon Sep 17 00:00:00 2001 From: daledah Date: Tue, 7 Jan 2025 05:29:00 +0700 Subject: [PATCH 61/75] fix: return empty array instead of throwing error if text !== markdown (#593) --- src/__tests__/parseExpensiMark.test.ts | 4 ++++ src/parseExpensiMark.ts | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/__tests__/parseExpensiMark.test.ts b/src/__tests__/parseExpensiMark.test.ts index b49f46fa..1d6a0436 100644 --- a/src/__tests__/parseExpensiMark.test.ts +++ b/src/__tests__/parseExpensiMark.test.ts @@ -31,6 +31,10 @@ test('no formatting', () => { expect('Hello, world!').toBeParsedAs([]); }); +describe('parsing error', () => { + expect(`> [exa\nmple.com](https://example.com)`).toBeParsedAs([]); +}); + test('bold', () => { expect('Hello, *world*!').toBeParsedAs([ {type: 'syntax', start: 7, length: 1}, diff --git a/src/parseExpensiMark.ts b/src/parseExpensiMark.ts index cbf69845..7657efd9 100644 --- a/src/parseExpensiMark.ts +++ b/src/parseExpensiMark.ts @@ -281,11 +281,12 @@ function parseExpensiMark(markdown: string): MarkdownRange[] { const tree = parseTokensToTree(tokens); const [text, ranges] = parseTreeToTextAndRanges(tree); if (text !== markdown) { - throw new Error( + console.error( `[react-native-live-markdown] Parsing error: the processed text does not match the original Markdown input. This may be caused by incorrect parsing functions or invalid input Markdown.\nProcessed input: '${JSON.stringify( text, )}'\nOriginal input: '${JSON.stringify(markdown)}'`, ); + return []; } const sortedRanges = sortRanges(ranges); const groupedRanges = groupRanges(sortedRanges); From 3f51bda020df9935dd6ff567477069f6b597cf73 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 22:30:03 +0000 Subject: [PATCH 62/75] Update package-lock.json version to 0.1.212 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index dee2f10c..02cbca17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.211", + "version": "0.1.212", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.211", + "version": "0.1.212", "hasInstallScript": true, "license": "MIT", "workspaces": [ From 24460a73fecab1fd8692be957ac7ed38a12870f2 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 22:30:04 +0000 Subject: [PATCH 63/75] Update package.json version to 0.1.212 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9e554fc2..14c421d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.211", + "version": "0.1.212", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From 9030bc5d58b6ba351645f34e45fd08635e9ab62b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= <39538890+Skalakid@users.noreply.github.com> Date: Tue, 7 Jan 2025 00:50:52 -0800 Subject: [PATCH 64/75] Fix custom parsing on the web (#585) --- src/parseExpensiMark.ts | 40 +------------------------- src/rangeUtils.ts | 56 ++++++++++++++++++++++++++++++++++++ src/web/utils/parserUtils.ts | 19 +++--------- 3 files changed, 61 insertions(+), 54 deletions(-) create mode 100644 src/rangeUtils.ts diff --git a/src/parseExpensiMark.ts b/src/parseExpensiMark.ts index 7657efd9..7073d349 100644 --- a/src/parseExpensiMark.ts +++ b/src/parseExpensiMark.ts @@ -6,6 +6,7 @@ import {unescapeText} from 'expensify-common/dist/utils'; import {decode} from 'html-entities'; import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/commonTypes'; import type {MarkdownType, MarkdownRange} from './commonTypes'; +import {groupRanges, sortRanges} from './rangeUtils'; function isWeb() { return Platform.OS === 'web'; @@ -233,45 +234,6 @@ function parseTreeToTextAndRanges(tree: StackItem): [string, MarkdownRange[]] { return [text, ranges]; } -// getTagPriority returns a priority for a tag, higher priority means the tag should be processed first -function getTagPriority(tag: string) { - switch (tag) { - case 'blockquote': - return 2; - case 'h1': - return 1; - default: - return 0; - } -} - -function sortRanges(ranges: MarkdownRange[]) { - // sort ranges by start position, then by length, then by tag hierarchy - return ranges.sort((a, b) => a.start - b.start || b.length - a.length || getTagPriority(b.type) - getTagPriority(a.type) || 0); -} - -function groupRanges(ranges: MarkdownRange[]) { - const lastVisibleRangeIndex: {[key in MarkdownType]?: number} = {}; - - return ranges.reduce((acc, range) => { - const start = range.start; - const end = range.start + range.length; - - const rangeWithSameStyleIndex = lastVisibleRangeIndex[range.type]; - const sameStyleRange = rangeWithSameStyleIndex !== undefined ? acc[rangeWithSameStyleIndex] : undefined; - - if (sameStyleRange && sameStyleRange.start <= start && sameStyleRange.start + sameStyleRange.length >= end && range.length > 1) { - // increment depth of overlapping range - sameStyleRange.depth = (sameStyleRange.depth || 1) + 1; - } else { - lastVisibleRangeIndex[range.type] = acc.length; - acc.push(range); - } - - return acc; - }, [] as MarkdownRange[]); -} - function parseExpensiMark(markdown: string): MarkdownRange[] { if (markdown.length > MAX_PARSABLE_LENGTH) { return []; diff --git a/src/rangeUtils.ts b/src/rangeUtils.ts new file mode 100644 index 00000000..dcfb913f --- /dev/null +++ b/src/rangeUtils.ts @@ -0,0 +1,56 @@ +import type {MarkdownRange, MarkdownType} from './commonTypes'; + +// getTagPriority returns a priority for a tag, higher priority means the tag should be processed first +function getTagPriority(tag: string) { + switch (tag) { + case 'blockquote': + return 2; + case 'h1': + return 1; + default: + return 0; + } +} + +function sortRanges(ranges: MarkdownRange[]) { + // sort ranges by start position, then by length, then by tag hierarchy + return ranges.sort((a, b) => a.start - b.start || b.length - a.length || getTagPriority(b.type) - getTagPriority(a.type) || 0); +} + +function groupRanges(ranges: MarkdownRange[]) { + const lastVisibleRangeIndex: {[key in MarkdownType]?: number} = {}; + + return ranges.reduce((acc, range) => { + const start = range.start; + const end = range.start + range.length; + + const rangeWithSameStyleIndex = lastVisibleRangeIndex[range.type]; + const sameStyleRange = rangeWithSameStyleIndex !== undefined ? acc[rangeWithSameStyleIndex] : undefined; + + if (sameStyleRange && sameStyleRange.start <= start && sameStyleRange.start + sameStyleRange.length >= end && range.length > 1) { + // increment depth of overlapping range + sameStyleRange.depth = (sameStyleRange.depth || 1) + 1; + } else { + lastVisibleRangeIndex[range.type] = acc.length; + acc.push(range); + } + + return acc; + }, [] as MarkdownRange[]); +} + +function ungroupRanges(ranges: MarkdownRange[]): MarkdownRange[] { + const ungroupedRanges: MarkdownRange[] = []; + ranges.forEach((range) => { + if (!range.depth) { + ungroupedRanges.push(range); + } + const {depth, ...rangeWithoutDepth} = range; + Array.from({length: depth!}).forEach(() => { + ungroupedRanges.push(rangeWithoutDepth); + }); + }); + return ungroupedRanges; +} + +export {sortRanges, groupRanges, ungroupRanges}; diff --git a/src/web/utils/parserUtils.ts b/src/web/utils/parserUtils.ts index 419ed780..694b6601 100644 --- a/src/web/utils/parserUtils.ts +++ b/src/web/utils/parserUtils.ts @@ -6,6 +6,7 @@ import {getCurrentCursorPosition, moveCursorToEnd, setCursorPosition} from './cu import {addStyleToBlock, extendBlockStructure, getFirstBlockMarkdownRange, isBlockMarkdownType} from './blockUtils'; import type {InlineImagesInputProps, MarkdownRange} from '../../commonTypes'; import {getAnimationCurrentTimes, updateAnimationsTime} from './animationUtils'; +import {sortRanges, ungroupRanges} from '../../rangeUtils'; type Paragraph = { text: string; @@ -14,20 +15,6 @@ type Paragraph = { markdownRanges: MarkdownRange[]; }; -function ungroupRanges(ranges: MarkdownRange[]): MarkdownRange[] { - const ungroupedRanges: MarkdownRange[] = []; - ranges.forEach((range) => { - if (!range.depth) { - ungroupedRanges.push(range); - } - const {depth, ...rangeWithoutDepth} = range; - Array.from({length: depth!}).forEach(() => { - ungroupedRanges.push(rangeWithoutDepth); - }); - }); - return ungroupedRanges; -} - function splitTextIntoLines(text: string): Paragraph[] { let lineStartIndex = 0; const lines: Paragraph[] = text.split('\n').map((line) => { @@ -167,7 +154,9 @@ function parseRangesToHTMLNodes( return {dom: rootElement, tree: rootNode}; } - const markdownRanges = ungroupRanges(ranges); + // Sort all ranges by start position, length, and by tag hierarchy so the styles and text are applied in correct order + const sortedRanges = sortRanges(ranges); + const markdownRanges = ungroupRanges(sortedRanges); lines = mergeLinesWithMultilineTags(lines, markdownRanges); let lastRangeEndIndex = 0; From 113c773cca31c9945cb63a07f1e7a6f8756040ad Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 08:51:48 +0000 Subject: [PATCH 65/75] Update package-lock.json version to 0.1.213 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 02cbca17..5eb44974 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.212", + "version": "0.1.213", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.212", + "version": "0.1.213", "hasInstallScript": true, "license": "MIT", "workspaces": [ From e9c2893f7505f9db6fd45a708df00872dfc8a20d Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 08:51:49 +0000 Subject: [PATCH 66/75] Update package.json version to 0.1.213 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 14c421d4..6801fdaf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.212", + "version": "0.1.213", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From 906f7679de2d34b9e84404dc40d685e7802f1ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= <39538890+Skalakid@users.noreply.github.com> Date: Tue, 7 Jan 2025 03:49:48 -0800 Subject: [PATCH 67/75] Prevent italic or strikethrough emojis on Android (#591) --- src/__tests__/splitRangesOnEmojis.test.ts | 163 ++++++++++++++++++++++ src/parseExpensiMark.ts | 9 +- src/rangeUtils.ts | 60 +++++++- 3 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 src/__tests__/splitRangesOnEmojis.test.ts diff --git a/src/__tests__/splitRangesOnEmojis.test.ts b/src/__tests__/splitRangesOnEmojis.test.ts new file mode 100644 index 00000000..a9eb11c3 --- /dev/null +++ b/src/__tests__/splitRangesOnEmojis.test.ts @@ -0,0 +1,163 @@ +import type {MarkdownRange} from '../commonTypes'; +import {splitRangesOnEmojis} from '../rangeUtils'; + +const sortRanges = (ranges: MarkdownRange[]) => { + return ranges.sort((a, b) => a.start - b.start); +}; + +test('no overlap', () => { + const markdownRanges: MarkdownRange[] = [ + {type: 'strikethrough', start: 0, length: 10}, + {type: 'emoji', start: 12, length: 2}, + ]; + + const splittedRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough'); + expect(splittedRanges).toEqual([ + {type: 'strikethrough', start: 0, length: 10}, + {type: 'emoji', start: 12, length: 2}, + ]); +}); + +test('overlap different type', () => { + const markdownRanges: MarkdownRange[] = [ + {type: 'strikethrough', start: 0, length: 10}, + {type: 'emoji', start: 3, length: 4}, + ]; + + const splittedRanges = splitRangesOnEmojis(markdownRanges, 'italic'); + expect(splittedRanges).toEqual(markdownRanges); +}); + +describe('single overlap', () => { + test('emoji at the beginning', () => { + let markdownRanges: MarkdownRange[] = [ + {type: 'strikethrough', start: 0, length: 10}, + {type: 'emoji', start: 0, length: 2}, + ]; + + markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough'); + sortRanges(markdownRanges); + + expect(markdownRanges).toEqual([ + {type: 'emoji', start: 0, length: 2}, + {type: 'strikethrough', start: 2, length: 8}, + ]); + }); + + test('emoji in the middle', () => { + let markdownRanges: MarkdownRange[] = [ + {type: 'strikethrough', start: 0, length: 10}, + {type: 'emoji', start: 3, length: 4}, + ]; + + markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough'); + sortRanges(markdownRanges); + + expect(markdownRanges).toEqual([ + {type: 'strikethrough', start: 0, length: 3}, + {type: 'emoji', start: 3, length: 4}, + {type: 'strikethrough', start: 7, length: 3}, + ]); + }); + + test('emoji at the end', () => { + let markdownRanges: MarkdownRange[] = [ + {type: 'strikethrough', start: 0, length: 10}, + {type: 'emoji', start: 8, length: 2}, + ]; + + markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough'); + sortRanges(markdownRanges); + + expect(markdownRanges).toEqual([ + {type: 'strikethrough', start: 0, length: 8}, + {type: 'emoji', start: 8, length: 2}, + ]); + }); + + test('multiple emojis in the middle', () => { + let markdownRanges: MarkdownRange[] = [ + {type: 'strikethrough', start: 0, length: 10}, + {type: 'emoji', start: 3, length: 2}, + {type: 'emoji', start: 5, length: 2}, + ]; + + markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough'); + sortRanges(markdownRanges); + + expect(markdownRanges).toEqual([ + {type: 'strikethrough', start: 0, length: 3}, + {type: 'emoji', start: 3, length: 2}, + {type: 'emoji', start: 5, length: 2}, + {type: 'strikethrough', start: 7, length: 3}, + ]); + }); + + test('just emojis', () => { + let markdownRanges: MarkdownRange[] = [ + {type: 'strikethrough', start: 0, length: 6}, + {type: 'emoji', start: 0, length: 2}, + {type: 'emoji', start: 2, length: 2}, + {type: 'emoji', start: 4, length: 2}, + ]; + + markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough'); + + expect(markdownRanges).toEqual([ + {type: 'emoji', start: 0, length: 2}, + {type: 'emoji', start: 2, length: 2}, + {type: 'emoji', start: 4, length: 2}, + ]); + }); +}); + +describe('multiple overlaps', () => { + test('splitting on one type', () => { + let markdownRanges: MarkdownRange[] = [ + {type: 'italic', start: 0, length: 20}, + {type: 'strikethrough', start: 2, length: 12}, + {type: 'emoji', start: 3, length: 1}, + {type: 'emoji', start: 8, length: 2}, + {type: 'strikethrough', start: 22, length: 5}, + ]; + + markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough'); + sortRanges(markdownRanges); + + expect(markdownRanges).toEqual([ + {type: 'italic', start: 0, length: 20}, + {type: 'strikethrough', start: 2, length: 1}, + {type: 'emoji', start: 3, length: 1}, + {type: 'strikethrough', start: 4, length: 4}, + {type: 'emoji', start: 8, length: 2}, + {type: 'strikethrough', start: 10, length: 4}, + {type: 'strikethrough', start: 22, length: 5}, + ]); + }); + + test('splitting on two types', () => { + let markdownRanges: MarkdownRange[] = [ + {type: 'italic', start: 0, length: 20}, + {type: 'strikethrough', start: 2, length: 12}, + {type: 'emoji', start: 3, length: 1}, + {type: 'emoji', start: 8, length: 2}, + {type: 'strikethrough', start: 22, length: 5}, + ]; + + markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough'); + markdownRanges = splitRangesOnEmojis(markdownRanges, 'italic'); + sortRanges(markdownRanges); + + expect(markdownRanges).toEqual([ + {type: 'italic', start: 0, length: 3}, + {type: 'strikethrough', start: 2, length: 1}, + {type: 'emoji', start: 3, length: 1}, + {type: 'italic', start: 4, length: 4}, + {type: 'strikethrough', start: 4, length: 4}, + {type: 'emoji', start: 8, length: 2}, + {type: 'italic', start: 10, length: 10}, + {type: 'strikethrough', start: 10, length: 4}, + {type: 'strikethrough', start: 22, length: 5}, + ]); + }); +}); diff --git a/src/parseExpensiMark.ts b/src/parseExpensiMark.ts index 7073d349..ecdbbd20 100644 --- a/src/parseExpensiMark.ts +++ b/src/parseExpensiMark.ts @@ -6,7 +6,7 @@ import {unescapeText} from 'expensify-common/dist/utils'; import {decode} from 'html-entities'; import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/commonTypes'; import type {MarkdownType, MarkdownRange} from './commonTypes'; -import {groupRanges, sortRanges} from './rangeUtils'; +import {groupRanges, sortRanges, splitRangesOnEmojis} from './rangeUtils'; function isWeb() { return Platform.OS === 'web'; @@ -250,8 +250,13 @@ function parseExpensiMark(markdown: string): MarkdownRange[] { ); return []; } - const sortedRanges = sortRanges(ranges); + + let splittedRanges = splitRangesOnEmojis(ranges, 'italic'); + splittedRanges = splitRangesOnEmojis(splittedRanges, 'strikethrough'); + + const sortedRanges = sortRanges(splittedRanges); const groupedRanges = groupRanges(sortedRanges); + return groupedRanges; } diff --git a/src/rangeUtils.ts b/src/rangeUtils.ts index dcfb913f..f47c4aaa 100644 --- a/src/rangeUtils.ts +++ b/src/rangeUtils.ts @@ -1,3 +1,5 @@ +'worklet'; + import type {MarkdownRange, MarkdownType} from './commonTypes'; // getTagPriority returns a priority for a tag, higher priority means the tag should be processed first @@ -53,4 +55,60 @@ function ungroupRanges(ranges: MarkdownRange[]): MarkdownRange[] { return ungroupedRanges; } -export {sortRanges, groupRanges, ungroupRanges}; +function splitRangesOnEmojis(ranges: MarkdownRange[], type: MarkdownType): MarkdownRange[] { + const emojiRanges: MarkdownRange[] = ranges.filter((range) => range.type === 'emoji'); + const newRanges: MarkdownRange[] = []; + + let i = 0; + let j = 0; + while (i < ranges.length) { + const currentRange = ranges[i]; + if (!currentRange) { + break; + } + + if (currentRange.type !== type) { + newRanges.push(currentRange); + i++; + } else { + // Iterate through all emoji ranges before the end of the current range, splitting the current range at each intersection. + while (j < emojiRanges.length) { + const emojiRange = emojiRanges[j]; + if (!emojiRange || emojiRange.start > currentRange.start + currentRange.length) { + break; + } + + const currentStart: number = currentRange.start; + const currentEnd: number = currentRange.start + currentRange.length; + const emojiStart: number = emojiRange.start; + const emojiEnd: number = emojiRange.start + emojiRange.length; + + if (emojiStart >= currentStart && emojiEnd <= currentEnd) { + // Intersection + const newRange: MarkdownRange = { + type: currentRange.type, + start: currentStart, + length: emojiStart - currentStart, + ...(currentRange?.depth && {depth: currentRange?.depth}), + }; + + currentRange.start = emojiEnd; + currentRange.length = currentEnd - emojiEnd; + + if (newRange.length > 0) { + newRanges.push(newRange); + } + } + j++; + } + + if (currentRange.length > 0) { + newRanges.push(currentRange); + } + i++; + } + } + return newRanges; +} + +export {sortRanges, groupRanges, ungroupRanges, splitRangesOnEmojis}; From 4441b1ad60484861aa3b7e3d3ad46c0bb99be86b Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 11:50:40 +0000 Subject: [PATCH 68/75] Update package-lock.json version to 0.1.214 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5eb44974..61d0f496 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.213", + "version": "0.1.214", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.213", + "version": "0.1.214", "hasInstallScript": true, "license": "MIT", "workspaces": [ From 100fa3be6986fda6687b749bab89f9d04bfc2532 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 11:50:41 +0000 Subject: [PATCH 69/75] Update package.json version to 0.1.214 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6801fdaf..cc6d41af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.213", + "version": "0.1.214", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From f6196215d1779bb3db77b166064a0a5776346540 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus <50919443+bernhardoj@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:33:00 +0800 Subject: [PATCH 70/75] Handle context menu format bold and italic (#556) --- example/src/App.tsx | 12 +++++++++ src/MarkdownTextInput.tsx | 1 + src/MarkdownTextInput.web.tsx | 50 ++++++++++++++++++++++++++++++++++- 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 77d5e421..2d48a2bd 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -15,6 +15,17 @@ import {PlatformInfo} from './PlatformInfo'; // We don't need this workaround in New Expensify App since Reanimated is imported before Live Markdown. console.log(Animated); +function handleFormatSelection(selectedText: string, formatCommand: string) { + switch (formatCommand) { + case 'formatBold': + return `*${selectedText}*`; + case 'formatItalic': + return `_${selectedText}_`; + default: + return selectedText; + } +} + export default function App() { const [value, setValue] = React.useState(TEST_CONST.EXAMPLE_CONTENT); const [textColorState, setTextColorState] = React.useState(false); @@ -48,6 +59,7 @@ export default function App() { string; parser: (value: string) => MarkdownRange[]; } diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index b6902801..05316223 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -30,6 +30,7 @@ const useClientEffect = typeof window === 'undefined' ? useEffect : useLayoutEff interface MarkdownTextInputProps extends TextInputProps, InlineImagesInputProps { markdownStyle?: MarkdownStyle; parser: (text: string) => MarkdownRange[]; + formatSelection?: (selectedText: string, formatCommand: string) => string; onClick?: (e: MouseEvent) => void; dir?: string; disabled?: boolean; @@ -85,6 +86,7 @@ const MarkdownTextInput = React.forwardRef { + if (!contentSelection.current || contentSelection.current.end - contentSelection.current.start < 1) { + throw new Error('[react-native-live-markdown] Trying to apply format command on empty selection'); + } + + if (!formatSelection) { + return parseText(parser, target, parsedText, processedMarkdownStyle, cursorPosition); + } + + const selectedText = parsedText.slice(contentSelection.current.start, contentSelection.current.end); + const formattedText = formatSelection(selectedText, formatCommand); + + if (selectedText === formattedText) { + return parseText(parser, target, parsedText, processedMarkdownStyle, cursorPosition); + } + + const prefix = parsedText.slice(0, contentSelection.current.start); + const suffix = parsedText.slice(contentSelection.current.end); + const diffLength = formattedText.length - selectedText.length; + const text = `${prefix}${formattedText}${suffix}`; + + return parseText(parser, target, text, processedMarkdownStyle, cursorPosition + diffLength, true); + }, + [parser, parseText, formatSelection, processedMarkdownStyle], + ); + // Placeholder text color logic const updateTextColor = useCallback( (node: HTMLDivElement, text: string) => { @@ -361,6 +390,11 @@ const MarkdownTextInput = React.forwardRef Date: Wed, 8 Jan 2025 08:33:53 +0000 Subject: [PATCH 71/75] Update package-lock.json version to 0.1.215 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 61d0f496..b181eeff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.214", + "version": "0.1.215", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.214", + "version": "0.1.215", "hasInstallScript": true, "license": "MIT", "workspaces": [ From 07b95bdd3a3a4c4eaf1867ee8fbf6d4f441d07d6 Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 08:33:53 +0000 Subject: [PATCH 72/75] Update package.json version to 0.1.215 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cc6d41af..103de625 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.214", + "version": "0.1.215", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index", From fef56a03fc3c1b44aa486393a3da1945fc7ac7ac Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Wed, 8 Jan 2025 12:20:08 +0100 Subject: [PATCH 73/75] Add support for short mentions from ExpensiMark (#592) --- src/commonTypes.ts | 1 + src/parseExpensiMark.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/commonTypes.ts b/src/commonTypes.ts index 73edbf39..a7cb9e01 100644 --- a/src/commonTypes.ts +++ b/src/commonTypes.ts @@ -5,6 +5,7 @@ type MarkdownType = | 'emoji' | 'mention-here' | 'mention-user' + | 'mention-short' | 'mention-report' | 'link' | 'code' diff --git a/src/parseExpensiMark.ts b/src/parseExpensiMark.ts index ecdbbd20..37216d11 100644 --- a/src/parseExpensiMark.ts +++ b/src/parseExpensiMark.ts @@ -155,6 +155,8 @@ function parseTreeToTextAndRanges(tree: StackItem): [string, MarkdownRange[]] { addChildrenWithStyle(node, 'mention-here'); } else if (node.tag === '') { addChildrenWithStyle(node, 'mention-user'); + } else if (node.tag === '') { + addChildrenWithStyle(node, 'mention-short'); } else if (node.tag === '') { addChildrenWithStyle(node, 'mention-report'); } else if (node.tag === '
') { From 2575b8470286a9feeba04b232a8308eeeb900a6b Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:21:08 +0000 Subject: [PATCH 74/75] Update package-lock.json version to 0.1.216 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index b181eeff..82050f32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.215", + "version": "0.1.216", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@expensify/react-native-live-markdown", - "version": "0.1.215", + "version": "0.1.216", "hasInstallScript": true, "license": "MIT", "workspaces": [ From 149b3ee43d4e7f7d91085d29a776e8e7a59309ac Mon Sep 17 00:00:00 2001 From: "os-botify[bot]" <140437396+os-botify[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:21:09 +0000 Subject: [PATCH 75/75] Update package.json version to 0.1.216 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 103de625..8f097dff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.215", + "version": "0.1.216", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index",