@expensify/react-native-live-markdown 0.1.315 → 0.1.317

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +5 -5
  2. package/RNLiveMarkdown.podspec +7 -20
  3. package/android/build.gradle +4 -16
  4. package/android/src/main/cpp/CMakeLists.txt +5 -23
  5. package/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java +3 -3
  6. package/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java +23 -0
  7. package/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java +94 -0
  8. package/apple/MarkdownFormatter.h +2 -0
  9. package/apple/MarkdownFormatter.mm +36 -6
  10. package/apple/MarkdownParser.mm +0 -4
  11. package/apple/MarkdownTextInputDecoratorComponentView.mm +17 -2
  12. package/apple/{BlockquoteTextLayoutFragment.h → MarkdownTextLayoutFragment.h} +4 -1
  13. package/apple/MarkdownTextLayoutFragment.mm +160 -0
  14. package/apple/MarkdownTextLayoutManagerDelegate.mm +22 -4
  15. package/apple/RCTMarkdownStyle.h +3 -0
  16. package/apple/RCTMarkdownStyle.mm +3 -0
  17. package/apple/RCTMarkdownTextBackground.h +9 -0
  18. package/apple/RCTMarkdownTextBackground.mm +4 -0
  19. package/apple/RCTMarkdownTextBackgroundWithRange.h +10 -0
  20. package/apple/RCTMarkdownTextBackgroundWithRange.mm +5 -0
  21. package/cpp/MarkdownGlobal.h +0 -4
  22. package/cpp/RuntimeDecorator.cpp +1 -6
  23. package/lib/commonjs/MarkdownTextInput.js +6 -4
  24. package/lib/commonjs/MarkdownTextInput.js.map +1 -1
  25. package/lib/commonjs/parseExpensiMark.js.map +1 -1
  26. package/lib/commonjs/styleUtils.js +6 -3
  27. package/lib/commonjs/styleUtils.js.map +1 -1
  28. package/lib/module/MarkdownTextInput.js +6 -4
  29. package/lib/module/MarkdownTextInput.js.map +1 -1
  30. package/lib/module/parseExpensiMark.js.map +1 -1
  31. package/lib/module/styleUtils.js +6 -3
  32. package/lib/module/styleUtils.js.map +1 -1
  33. package/lib/typescript/src/MarkdownTextInput.d.ts +3 -2
  34. package/lib/typescript/src/MarkdownTextInput.d.ts.map +1 -1
  35. package/lib/typescript/src/commonTypes.d.ts +1 -55
  36. package/lib/typescript/src/commonTypes.d.ts.map +1 -1
  37. package/lib/typescript/src/parseExpensiMark.d.ts.map +1 -1
  38. package/lib/typescript/src/styleUtils.d.ts.map +1 -1
  39. package/package.json +1 -8
  40. package/src/MarkdownTextInput.tsx +7 -6
  41. package/src/commonTypes.ts +1 -75
  42. package/src/parseExpensiMark.ts +2 -1
  43. package/src/styleUtils.ts +3 -0
  44. package/apple/BlockquoteTextLayoutFragment.mm +0 -51
package/README.md CHANGED
@@ -15,12 +15,12 @@
15
15
  First, install the library from npm with the package manager of your choice:
16
16
 
17
17
  ```sh
18
- yarn add @expensify/react-native-live-markdown react-native-reanimated expensify-common html-entities@2.5.3
19
- npm install @expensify/react-native-live-markdown react-native-reanimated expensify-common html-entities@2.5.3 --save
20
- npx expo install @expensify/react-native-live-markdown react-native-reanimated expensify-common html-entities@2.5.3
18
+ yarn add @expensify/react-native-live-markdown react-native-worklets expensify-common html-entities@2.5.3
19
+ npm install @expensify/react-native-live-markdown react-native-worklets expensify-common html-entities@2.5.3 --save
20
+ npx expo install @expensify/react-native-live-markdown react-native-worklets expensify-common html-entities@2.5.3
21
21
  ```
22
22
 
23
- React Native Live Markdown requires [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated) 3.17.0 or newer as well as [expensify-common](https://github.com/Expensify/expensify-common) 2.0.115 and [html-entities](https://github.com/mdevils/html-entities) 2.5.3 exactly if using the default built-in ExpensiMark parser.
23
+ React Native Live Markdown requires [react-native-worklets](https://github.com/software-mansion/react-native-reanimated/tree/main/packages/react-native-worklets) 0.6.0 or newer as well as [expensify-common](https://github.com/Expensify/expensify-common) 2.0.115 and [html-entities](https://github.com/mdevils/html-entities) 2.5.3 exactly if using the default built-in ExpensiMark parser.
24
24
 
25
25
  Then, install the iOS dependencies with CocoaPods:
26
26
 
@@ -148,7 +148,7 @@ Currently, only the following types are supported:
148
148
  type MarkdownType = 'bold' | 'italic' | 'strikethrough' | 'emoji' | 'mention-here' | 'mention-user' | 'mention-report' | 'link' | 'code' | 'pre' | 'blockquote' | 'h1' | 'syntax';
149
149
  ```
150
150
 
151
- Parser needs to be marked as a [worklet](https://docs.swmansion.com/react-native-reanimated/docs/guides/worklets/) because it's executed on the UI thread as the user types.
151
+ Parser needs to be marked as a [worklet](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/glossary#worklet) because it's executed on the UI thread as the user types.
152
152
 
153
153
  Here's a sample function that parses all substrings located between two asterisks as bold text:
154
154
 
@@ -5,14 +5,9 @@ react_native_json = JSON.parse(File.read(File.join(react_native_node_modules_dir
5
5
  react_native_minor_version = react_native_json['version'].split('.')[1].to_i
6
6
 
7
7
  pods_root = Pod::Config.instance.project_pods_root
8
-
9
- react_native_worklets_path = `cd "#{Pod::Config.instance.installation_root.to_s}" && node --print "require.resolve('react-native-worklets/package.json')"`
10
- worklets_installed = react_native_worklets_path != ""
11
- worklets_package_name = worklets_installed ? 'react-native-worklets' : 'react-native-reanimated'
12
-
13
- react_native_worklets_or_reanimated_node_modules_dir = ENV['REACT_NATIVE_WORKLETS_NODE_MODULES_DIR'] || ENV['REACT_NATIVE_REANIMATED_NODE_MODULES_DIR'] ||
14
- File.dirname(`cd "#{Pod::Config.instance.installation_root.to_s}" && node --print "require.resolve('#{worklets_package_name}/package.json')"`)
15
- react_native_worklets_or_reanimated_node_modules_dir_from_pods_root = Pathname.new(react_native_worklets_or_reanimated_node_modules_dir).relative_path_from(pods_root).to_s
8
+ react_native_worklets_node_modules_dir = ENV['REACT_NATIVE_WORKLETS_NODE_MODULES_DIR'] ||
9
+ File.dirname(`cd "#{Pod::Config.instance.installation_root.to_s}" && node --print "require.resolve('react-native-worklets/package.json')"`)
10
+ react_native_worklets_node_modules_dir_from_pods_root = Pathname.new(react_native_worklets_node_modules_dir).relative_path_from(pods_root).to_s
16
11
 
17
12
  react_native_react_common_dir = File.join(react_native_node_modules_dir, 'react-native/ReactCommon')
18
13
  react_native_react_common_dir_from_pods_root = Pathname.new(react_native_react_common_dir).relative_path_from(pods_root).to_s
@@ -33,24 +28,16 @@ Pod::Spec.new do |s|
33
28
 
34
29
  s.source_files = "apple/**/*.{h,m,mm}", "cpp/**/*.{h,cpp}"
35
30
 
36
- if worklets_installed
37
- s.dependency "RNWorklets"
38
- else
39
- s.dependency "RNReanimated/worklets"
40
- end
31
+ s.dependency "RNWorklets"
41
32
 
42
- xcconfig = {
33
+ s.xcconfig = {
43
34
  "OTHER_CFLAGS" => "$(inherited) -DREACT_NATIVE_MINOR_VERSION=#{react_native_minor_version}",
44
35
  "HEADER_SEARCH_PATHS" => [
45
- "\"$(PODS_ROOT)/#{react_native_worklets_or_reanimated_node_modules_dir_from_pods_root}/apple\"",
46
- "\"$(PODS_ROOT)/#{react_native_worklets_or_reanimated_node_modules_dir_from_pods_root}/Common/cpp\"",
36
+ "\"$(PODS_ROOT)/#{react_native_worklets_node_modules_dir_from_pods_root}/apple\"",
37
+ "\"$(PODS_ROOT)/#{react_native_worklets_node_modules_dir_from_pods_root}/Common/cpp\"",
47
38
  "\"$(PODS_ROOT)/#{react_native_react_common_dir_from_pods_root}\"",
48
39
  ].join(' '),
49
40
  }
50
- if worklets_installed
51
- xcconfig["OTHER_CFLAGS"] << " -DWORKLETS_INSTALLED=1"
52
- end
53
- s.xcconfig = xcconfig
54
41
 
55
42
  s.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/cpp\"" }
56
43
 
@@ -67,7 +67,6 @@ def getReactNativeMinorVersion() {
67
67
  }
68
68
 
69
69
  def REACT_NATIVE_MINOR_VERSION = getReactNativeMinorVersion()
70
- def workletsInstalled = rootProject.findProject(':react-native-worklets') != null
71
70
 
72
71
  android {
73
72
  if (supportsNamespace()) {
@@ -96,8 +95,7 @@ android {
96
95
  "-DANDROID_TOOLCHAIN=clang",
97
96
  "-DREACT_NATIVE_MINOR_VERSION=${REACT_NATIVE_MINOR_VERSION}",
98
97
  "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
99
- "-DREACT_NATIVE_ROOT_DIR=${resolveReactNativeDirectory().path}",
100
- "-DWORKLETS_INSTALLED=${workletsInstalled}"
98
+ "-DREACT_NATIVE_ROOT_DIR=${resolveReactNativeDirectory().path}"
101
99
  abiFilters (*reactNativeArchitectures())
102
100
  }
103
101
  }
@@ -180,12 +178,7 @@ repositories {
180
178
  dependencies {
181
179
  implementation "com.facebook.react:react-android" // version substituted by RNGP
182
180
  implementation "com.facebook.react:hermes-android" // version substituted by RNGP
183
-
184
- if (workletsInstalled) {
185
- implementation project(":react-native-worklets")
186
- } else {
187
- implementation project(":react-native-reanimated")
188
- }
181
+ implementation project(":react-native-worklets")
189
182
  }
190
183
 
191
184
  // This fixes linking errors due to undefined symbols from libworklets.so.
@@ -193,11 +186,6 @@ dependencies {
193
186
  // like a header-only library. During build, config files are not regenerated
194
187
  // because the cache key does not change and AGP thinks that they are up-to-date.
195
188
  afterEvaluate {
196
- if (workletsInstalled) {
197
- prepareKotlinBuildScriptModel.dependsOn(":react-native-worklets:prefabDebugPackage")
198
- prepareKotlinBuildScriptModel.dependsOn(":react-native-worklets:prefabReleasePackage")
199
- } else {
200
- prepareKotlinBuildScriptModel.dependsOn(":react-native-reanimated:prefabDebugPackage")
201
- prepareKotlinBuildScriptModel.dependsOn(":react-native-reanimated:prefabReleasePackage")
202
- }
189
+ prepareKotlinBuildScriptModel.dependsOn(":react-native-worklets:prefabDebugPackage")
190
+ prepareKotlinBuildScriptModel.dependsOn(":react-native-worklets:prefabReleasePackage")
203
191
  }
@@ -4,10 +4,8 @@ cmake_minimum_required(VERSION 3.13)
4
4
 
5
5
  set(CMAKE_VERBOSE_MAKEFILE on)
6
6
 
7
- if(WORKLETS_INSTALLED)
8
- include("${REACT_NATIVE_ROOT_DIR}/ReactAndroid/cmake-utils/folly-flags.cmake")
9
- add_compile_options(${folly_FLAGS})
10
- endif()
7
+ include("${REACT_NATIVE_ROOT_DIR}/ReactAndroid/cmake-utils/folly-flags.cmake")
8
+ add_compile_options(${folly_FLAGS})
11
9
 
12
10
  add_compile_options(-fvisibility=hidden -fexceptions -frtti)
13
11
 
@@ -20,32 +18,16 @@ file(GLOB CPP_SRC CONFIGURE_DEPENDS "${CPP_DIR}/*.cpp")
20
18
 
21
19
  add_library(${CMAKE_PROJECT_NAME} SHARED ${ANDROID_SRC} ${CPP_SRC})
22
20
 
23
- if(WORKLETS_INSTALLED)
24
- target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CPP_DIR} "${REACT_NATIVE_ROOT_DIR}/ReactCommon/jsiexecutor")
25
- else()
26
- target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CPP_DIR})
27
- endif()
21
+ target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CPP_DIR} "${REACT_NATIVE_ROOT_DIR}/ReactCommon/jsiexecutor")
28
22
 
29
23
  find_package(fbjni REQUIRED CONFIG)
30
24
  find_package(ReactAndroid REQUIRED CONFIG)
31
-
32
- if(WORKLETS_INSTALLED)
33
- find_package(react-native-worklets REQUIRED CONFIG)
34
- add_definitions(-DWORKLETS_INSTALLED)
35
- else()
36
- find_package(react-native-reanimated REQUIRED CONFIG)
37
- endif()
38
-
25
+ find_package(react-native-worklets REQUIRED CONFIG)
39
26
 
40
27
  target_link_libraries(
41
28
  ${CMAKE_PROJECT_NAME}
42
29
  fbjni::fbjni
43
30
  ReactAndroid::jsi
44
31
  ReactAndroid::reactnative
32
+ react-native-worklets::worklets
45
33
  )
46
-
47
- if(WORKLETS_INSTALLED)
48
- target_link_libraries(${CMAKE_PROJECT_NAME} react-native-worklets::worklets)
49
- else()
50
- target_link_libraries(${CMAKE_PROJECT_NAME} react-native-reanimated::worklets)
51
- endif()
@@ -75,16 +75,16 @@ public class MarkdownFormatter {
75
75
  break;
76
76
  case "mention-here":
77
77
  setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getMentionHereColor()), start, end);
78
- setSpan(ssb, new MarkdownBackgroundColorSpan(markdownStyle.getMentionHereBackgroundColor()), start, end);
78
+ setSpan(ssb, new MarkdownBackgroundSpan(markdownStyle.getMentionHereBackgroundColor(), markdownStyle.getMentionHereBorderRadius(), start, end), start, end);
79
79
  break;
80
80
  case "mention-user":
81
81
  // TODO: change mention color when it mentions current user
82
82
  setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getMentionUserColor()), start, end);
83
- setSpan(ssb, new MarkdownBackgroundColorSpan(markdownStyle.getMentionUserBackgroundColor()), start, end);
83
+ setSpan(ssb, new MarkdownBackgroundSpan(markdownStyle.getMentionUserBackgroundColor(), markdownStyle.getMentionUserBorderRadius(), start, end), start, end);
84
84
  break;
85
85
  case "mention-report":
86
86
  setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getMentionReportColor()), start, end);
87
- setSpan(ssb, new MarkdownBackgroundColorSpan(markdownStyle.getMentionReportBackgroundColor()), start, end);
87
+ setSpan(ssb, new MarkdownBackgroundSpan(markdownStyle.getMentionReportBackgroundColor(), markdownStyle.getMentionReportBorderRadius(), start, end), start, end);
88
88
  break;
89
89
  case "syntax":
90
90
  setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getSyntaxColor()), start, end);
@@ -63,19 +63,27 @@ public class MarkdownStyle {
63
63
  @ColorInt
64
64
  private final int mMentionHereBackgroundColor;
65
65
 
66
+ private final float mMentionHereBorderRadius;
67
+
66
68
  @ColorInt
67
69
  private final int mMentionUserColor;
68
70
 
69
71
  @ColorInt
70
72
  private final int mMentionUserBackgroundColor;
71
73
 
74
+ private final float mMentionUserBorderRadius;
75
+
72
76
  @ColorInt
73
77
  private final int mMentionReportColor;
74
78
 
75
79
  @ColorInt
76
80
  private final int mMentionReportBackgroundColor;
77
81
 
82
+ private final float mMentionReportBorderRadius;
83
+
78
84
  public MarkdownStyle(@NonNull ReadableMap map, @NonNull Context context) {
85
+ float screenDensity = context.getResources().getDisplayMetrics().density;
86
+
79
87
  mSyntaxColor = parseColor(map, "syntax", "color", context);
80
88
  mLinkColor = parseColor(map, "link", "color", context);
81
89
  mH1FontSize = parseFloat(map, "h1", "fontSize");
@@ -95,10 +103,13 @@ public class MarkdownStyle {
95
103
  mPreBackgroundColor = parseColor(map, "pre", "backgroundColor", context);
96
104
  mMentionHereColor = parseColor(map, "mentionHere", "color", context);
97
105
  mMentionHereBackgroundColor = parseColor(map, "mentionHere", "backgroundColor", context);
106
+ mMentionHereBorderRadius = parseFloat(map, "mentionHere", "borderRadius") * screenDensity;
98
107
  mMentionUserColor = parseColor(map, "mentionUser", "color", context);
99
108
  mMentionUserBackgroundColor = parseColor(map, "mentionUser", "backgroundColor", context);
109
+ mMentionUserBorderRadius = parseFloat(map, "mentionUser", "borderRadius") * screenDensity;
100
110
  mMentionReportColor = parseColor(map, "mentionReport", "color", context);
101
111
  mMentionReportBackgroundColor = parseColor(map, "mentionReport", "backgroundColor", context);
112
+ mMentionReportBorderRadius = parseFloat(map, "mentionReport", "borderRadius") * screenDensity;
102
113
  }
103
114
 
104
115
  private static int parseColor(@NonNull ReadableMap map, @NonNull String key, @NonNull String prop, @NonNull Context context) {
@@ -213,6 +224,10 @@ public class MarkdownStyle {
213
224
  return mMentionHereBackgroundColor;
214
225
  }
215
226
 
227
+ public float getMentionHereBorderRadius() {
228
+ return mMentionHereBorderRadius;
229
+ }
230
+
216
231
  @ColorInt
217
232
  public int getMentionUserColor() {
218
233
  return mMentionUserColor;
@@ -223,6 +238,10 @@ public class MarkdownStyle {
223
238
  return mMentionUserBackgroundColor;
224
239
  }
225
240
 
241
+ public float getMentionUserBorderRadius() {
242
+ return mMentionUserBorderRadius;
243
+ }
244
+
226
245
  @ColorInt
227
246
  public int getMentionReportColor() {
228
247
  return mMentionReportColor;
@@ -232,4 +251,8 @@ public class MarkdownStyle {
232
251
  public int getMentionReportBackgroundColor() {
233
252
  return mMentionReportBackgroundColor;
234
253
  }
254
+
255
+ public float getMentionReportBorderRadius() {
256
+ return mMentionReportBorderRadius;
257
+ }
235
258
  }
@@ -0,0 +1,94 @@
1
+ package com.expensify.livemarkdown.spans;
2
+
3
+ import android.graphics.Canvas;
4
+ import android.graphics.Paint;
5
+ import android.graphics.Path;
6
+ import android.graphics.RectF;
7
+ import android.text.StaticLayout;
8
+ import android.text.TextPaint;
9
+ import android.text.style.LineBackgroundSpan;
10
+
11
+ import androidx.annotation.ColorInt;
12
+ import androidx.annotation.NonNull;
13
+
14
+ public class MarkdownBackgroundSpan implements MarkdownSpan, LineBackgroundSpan {
15
+
16
+ private final int backgroundColor;
17
+ private final int mentionStart;
18
+ private final int mentionEnd;
19
+ private final float borderRadius;
20
+
21
+ private StaticLayout layout;
22
+ private Path backgroundPath;
23
+
24
+ public MarkdownBackgroundSpan(@ColorInt int backgroundColor, float borderRadius, int mentionStart, int mentionEnd) {
25
+ this.backgroundColor = backgroundColor;
26
+ this.borderRadius = borderRadius;
27
+ this.mentionStart = mentionStart;
28
+ this.mentionEnd = mentionEnd;
29
+ this.backgroundPath = new Path();
30
+ }
31
+
32
+ @Override
33
+ public void drawBackground(
34
+ @NonNull Canvas canvas,
35
+ @NonNull Paint paint,
36
+ int left,
37
+ int right,
38
+ int top,
39
+ int baseline,
40
+ int bottom,
41
+ @NonNull CharSequence text,
42
+ int start,
43
+ int end,
44
+ int lnum
45
+ ) {
46
+ CharSequence lineText = text.subSequence(start, end);
47
+ if (layout == null || layout.getText() != lineText || layout.getWidth() != right || layout.getLineEnd(0) != lineText.length()) {
48
+ int currentLineStart = 0;
49
+ int currentLineEnd = lineText.length();
50
+ // Create layout for the current line only
51
+ layout = StaticLayout.Builder.obtain(lineText, currentLineStart, currentLineEnd, (TextPaint) paint, right).build();
52
+
53
+ int relativeMentionStart = mentionStart - start;
54
+ int relativeMentionEnd = mentionEnd - start;
55
+
56
+ boolean mentionStartsInCurrentLine = currentLineStart <= relativeMentionStart;
57
+ boolean mentionEndsInCurrentLine = currentLineEnd >= relativeMentionEnd;
58
+
59
+ float startX = layout.getPrimaryHorizontal(mentionStartsInCurrentLine ? relativeMentionStart: currentLineStart);
60
+ float endX = layout.getPrimaryHorizontal(mentionEndsInCurrentLine ? relativeMentionEnd : currentLineEnd);
61
+
62
+ Paint.FontMetrics fm = paint.getFontMetrics();
63
+ float startY = baseline + fm.ascent;
64
+ float endY = baseline + fm.descent;
65
+
66
+ RectF lineRect = new RectF(startX, startY, endX, endY);
67
+ backgroundPath.reset();
68
+ backgroundPath.addRoundRect(lineRect, createRadii(mentionStartsInCurrentLine, mentionEndsInCurrentLine), Path.Direction.CW);
69
+ }
70
+
71
+ int originalColor = paint.getColor();
72
+ paint.setColor(backgroundColor);
73
+
74
+ canvas.drawPath(backgroundPath, paint);
75
+
76
+ paint.setColor(originalColor);
77
+ }
78
+
79
+ private float[] createRadii(boolean roundedLeft, boolean roundedRight) {
80
+ float[] radii = new float[8];
81
+
82
+ if (roundedLeft) {
83
+ radii[0] = radii[1] = borderRadius; // top-left
84
+ radii[6] = radii[7] = borderRadius; // bottom-left
85
+ }
86
+
87
+ if (roundedRight) {
88
+ radii[2] = radii[3] = borderRadius; // top-right
89
+ radii[4] = radii[5] = borderRadius; // bottom-right
90
+ }
91
+
92
+ return radii;
93
+ }
94
+ }
@@ -6,6 +6,8 @@ NS_ASSUME_NONNULL_BEGIN
6
6
 
7
7
  const NSAttributedStringKey RCTLiveMarkdownTextAttributeName = @"RCTLiveMarkdownText";
8
8
 
9
+ const NSAttributedStringKey RCTLiveMarkdownTextBackgroundAttributeName = @"RCTLiveMarkdownTextBackground";
10
+
9
11
  const NSAttributedStringKey RCTLiveMarkdownBlockquoteDepthAttributeName = @"RCTLiveMarkdownBlockquoteDepth";
10
12
 
11
13
  @interface MarkdownFormatter : NSObject
@@ -1,5 +1,6 @@
1
1
  #import "MarkdownFormatter.h"
2
2
  #import <React/RCTFont.h>
3
+ #import <RNLiveMarkdown/RCTMarkdownTextBackgroundWithRange.h>
3
4
 
4
5
  @implementation MarkdownFormatter
5
6
 
@@ -90,15 +91,24 @@
90
91
  [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.codeColor range:range];
91
92
  [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.codeBackgroundColor range:range];
92
93
  } else if (type == "mention-here") {
93
- [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionHereColor range:range];
94
- [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionHereBackgroundColor range:range];
94
+ [self applyMentionFormatting:attributedString
95
+ range:range
96
+ mentionColor:markdownStyle.mentionHereColor
97
+ backgroundColor:markdownStyle.mentionHereBackgroundColor
98
+ borderRadius:markdownStyle.mentionHereBorderRadius];
95
99
  } else if (type == "mention-user") {
96
100
  // TODO: change mention color when it mentions current user
97
- [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionUserColor range:range];
98
- [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionUserBackgroundColor range:range];
101
+ [self applyMentionFormatting:attributedString
102
+ range:range
103
+ mentionColor:markdownStyle.mentionUserColor
104
+ backgroundColor:markdownStyle.mentionUserBackgroundColor
105
+ borderRadius:markdownStyle.mentionUserBorderRadius];
99
106
  } else if (type == "mention-report") {
100
- [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionReportColor range:range];
101
- [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionReportBackgroundColor range:range];
107
+ [self applyMentionFormatting:attributedString
108
+ range:range
109
+ mentionColor:markdownStyle.mentionReportColor
110
+ backgroundColor:markdownStyle.mentionReportBackgroundColor
111
+ borderRadius:markdownStyle.mentionReportBorderRadius];
102
112
  } else if (type == "link") {
103
113
  [attributedString addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range];
104
114
  [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.linkColor range:range];
@@ -118,6 +128,26 @@
118
128
  }
119
129
  }
120
130
 
131
+ - (void)applyMentionFormatting:(NSMutableAttributedString *)attributedString
132
+ range:(const NSRange)range
133
+ mentionColor:(UIColor *)mentionColor
134
+ backgroundColor:(UIColor *)backgroundColor
135
+ borderRadius:(CGFloat)borderRadius
136
+ {
137
+ [attributedString addAttribute:NSForegroundColorAttributeName value:mentionColor range:range];
138
+ if (@available(iOS 16.0, *)) {
139
+ RCTMarkdownTextBackground *textBackground = [[RCTMarkdownTextBackground alloc] init];
140
+ textBackground.color = backgroundColor;
141
+ textBackground.borderRadius = borderRadius;
142
+
143
+ [attributedString addAttribute:RCTLiveMarkdownTextBackgroundAttributeName
144
+ value:textBackground
145
+ range:range];
146
+ } else {
147
+ [attributedString addAttribute:NSBackgroundColorAttributeName value:backgroundColor range:range];
148
+ }
149
+ }
150
+
121
151
  static void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText, NSRange attributedTextRange)
122
152
  {
123
153
  __block CGFloat maximumLineHeight = 0;
@@ -19,11 +19,7 @@
19
19
  const auto &markdownRuntime = expensify::livemarkdown::getMarkdownRuntime();
20
20
  jsi::Runtime &rt = markdownRuntime->getJSIRuntime();
21
21
 
22
- #ifdef WORKLETS_INSTALLED
23
22
  std::shared_ptr<SerializableWorklet> markdownWorklet;
24
- #else
25
- std::shared_ptr<ShareableWorklet> markdownWorklet;
26
- #endif
27
23
  try {
28
24
  markdownWorklet = expensify::livemarkdown::getMarkdownWorklet([parserId intValue]);
29
25
  } catch (const std::out_of_range &error) {
@@ -77,7 +77,7 @@ using namespace facebook::react;
77
77
  react_native_assert([childView isKindOfClass:[RCTTextInputComponentView class]] && "Child component of MarkdownTextInputDecoratorComponentView is not an instance of RCTTextInputComponentView.");
78
78
  RCTTextInputComponentView *textInputComponentView = (RCTTextInputComponentView *)childView;
79
79
  UIView<RCTBackedTextInputViewProtocol> *backedTextInputView = [textInputComponentView valueForKey:@"_backedTextInputView"];
80
-
80
+
81
81
  _observersAdded = true;
82
82
 
83
83
  if ([backedTextInputView isKindOfClass:[RCTUITextField class]]) {
@@ -100,6 +100,21 @@ using namespace facebook::react;
100
100
  // format initial value
101
101
  [_markdownTextFieldObserver textFieldDidChange:_textField];
102
102
 
103
+ if (@available(iOS 16.0, *)) {
104
+ auto key = [](std::string s) {
105
+ std::reverse(s.begin(), s.end());
106
+ return @(s.c_str());
107
+ };
108
+
109
+ NSTextContainer *textContainer = [_textField valueForKey:key("reniatnoCtxet_")];
110
+ NSTextLayoutManager *textLayoutManager = [textContainer valueForKey:key("reganaMtuoyaLtxet_")];
111
+
112
+ _markdownTextLayoutManagerDelegate = [[MarkdownTextLayoutManagerDelegate alloc] init];
113
+ _markdownTextLayoutManagerDelegate.textStorage = [_textField valueForKey:key("egarotStxet_")];
114
+ _markdownTextLayoutManagerDelegate.markdownUtils = _markdownUtils;
115
+ textLayoutManager.delegate = _markdownTextLayoutManagerDelegate;
116
+ }
117
+
103
118
  // TODO: register blockquotes layout manager
104
119
  // https://github.com/Expensify/react-native-live-markdown/issues/87
105
120
  } else if ([backedTextInputView isKindOfClass:[RCTUITextView class]]) {
@@ -229,7 +244,7 @@ using namespace facebook::react;
229
244
  {
230
245
  react_native_assert(!_observersAdded && "MarkdownTextInputDecoratorComponentView was being recycled with TextInput observers still attached");
231
246
  [super prepareForRecycle];
232
-
247
+
233
248
  static const auto defaultProps = std::make_shared<const MarkdownTextInputDecoratorViewProps>();
234
249
  _props = defaultProps;
235
250
  _markdownUtils = [[RCTMarkdownUtils alloc] init];
@@ -1,15 +1,18 @@
1
1
  #import <RNLiveMarkdown/RCTMarkdownUtils.h>
2
+ #import <RNLiveMarkdown/RCTMarkdownTextBackgroundWithRange.h>
2
3
  #import <UIKit/UIKit.h>
3
4
 
4
5
  NS_ASSUME_NONNULL_BEGIN
5
6
 
6
7
  API_AVAILABLE(ios(15.0))
7
- @interface BlockquoteTextLayoutFragment : NSTextLayoutFragment
8
+ @interface MarkdownTextLayoutFragment : NSTextLayoutFragment
8
9
 
9
10
  @property (nonnull, atomic) RCTMarkdownUtils *markdownUtils;
10
11
 
11
12
  @property NSUInteger depth;
12
13
 
14
+ @property NSMutableArray<RCTMarkdownTextBackgroundWithRange *> *mentions;
15
+
13
16
  @end
14
17
 
15
18
  NS_ASSUME_NONNULL_END
@@ -0,0 +1,160 @@
1
+ #import <RNLiveMarkdown/MarkdownTextLayoutFragment.h>
2
+ #import <RNLiveMarkdown/RCTMarkdownTextBackgroundWithRange.h>
3
+
4
+ @implementation MarkdownTextLayoutFragment
5
+
6
+ #pragma mark - overriding class methods
7
+
8
+ - (CGRect)renderingSurfaceBounds {
9
+ if (self.depth == 0) {
10
+ return [super renderingSurfaceBounds];
11
+ }
12
+ return CGRectUnion(self.boundingRect, [super renderingSurfaceBounds]);
13
+ }
14
+
15
+ - (void)drawAtPoint:(CGPoint)point inContext:(CGContextRef)ctx {
16
+ if (self.textLineFragments.count == 0) {
17
+ [super drawAtPoint:point inContext:ctx];
18
+ return;
19
+ }
20
+
21
+ [self drawBlockquoteRibbons];
22
+ [self drawMentions];
23
+
24
+ [super drawAtPoint:point inContext:ctx];
25
+ }
26
+
27
+ #pragma mark - drawing custom elements
28
+
29
+ - (void)drawBlockquoteRibbons {
30
+ if (self.depth == 0) {
31
+ return;
32
+ }
33
+
34
+ CGFloat marginLeft = _markdownUtils.markdownStyle.blockquoteMarginLeft;
35
+ CGFloat borderWidth = _markdownUtils.markdownStyle.blockquoteBorderWidth;
36
+ CGFloat paddingLeft = _markdownUtils.markdownStyle.blockquotePaddingLeft;
37
+ CGFloat shift = marginLeft + borderWidth + paddingLeft;
38
+
39
+ [_markdownUtils.markdownStyle.blockquoteBorderColor setFill];
40
+
41
+ CGRect boundingRect = self.boundingRect;
42
+ for (NSUInteger level = 0; level < _depth; ++level) {
43
+ CGFloat x = boundingRect.origin.x + level * shift;
44
+ CGRect ribbonRect = CGRectMake(x, boundingRect.origin.y, borderWidth, boundingRect.size.height);
45
+ UIRectFill(ribbonRect);
46
+ }
47
+ }
48
+
49
+ - (void)drawMentions {
50
+ if (self.mentions.count == 0) {
51
+ return;
52
+ }
53
+
54
+ bool isSingleline = [self.textLineFragments count] == 1;
55
+ __block NSUInteger mentionIndex = 0;
56
+ [self.textLineFragments enumerateObjectsUsingBlock:^(NSTextLineFragment * _Nonnull lineFragment, NSUInteger idx, BOOL * _Nonnull stop) {
57
+ if (lineFragment.characterRange.length == 0) {
58
+ return;
59
+ }
60
+
61
+ CGRect lineBounds = lineFragment.typographicBounds;
62
+ NSRange lineRange = lineFragment.characterRange;
63
+ CGPoint lineEndLocation = [lineFragment locationForCharacterAtIndex: lineFragment.characterRange.length];
64
+
65
+ while (mentionIndex < self.mentions.count &&
66
+ NSMaxRange(self.mentions[mentionIndex].range) <= lineRange.location) {
67
+ mentionIndex++;
68
+ }
69
+
70
+ for (NSUInteger i = mentionIndex; i < self.mentions.count; i++) {
71
+ RCTMarkdownTextBackgroundWithRange *mention = self.mentions[i];
72
+ NSRange mentionRange = mention.range;
73
+
74
+ if (mentionRange.location >= NSMaxRange(lineRange)) {
75
+ break;
76
+ }
77
+
78
+ NSRange intersection = NSIntersectionRange(lineRange, mentionRange);
79
+ CGPoint startLocation = [lineFragment locationForCharacterAtIndex:intersection.location];
80
+ if (isSingleline && startLocation.x == 0 && intersection.location > 0) {
81
+ // singleline: mention starts off screen, no need to draw background
82
+ // it only happens when the text input isn't focused
83
+ continue;
84
+ }
85
+
86
+ CGPoint endLocation = [lineFragment locationForCharacterAtIndex:intersection.location + intersection.length];
87
+ if (isSingleline && (startLocation.x > endLocation.x || (startLocation.x == endLocation.x && intersection.location == 0))) {
88
+ // singleline: mention is partially visible
89
+ // 1. starts in the middle, or
90
+ // 2. starts at the beginning of the line
91
+ endLocation = lineEndLocation;
92
+ }
93
+
94
+ CGFloat width = endLocation.x - startLocation.x;
95
+ CGFloat x = lineBounds.origin.x + startLocation.x;
96
+
97
+ UIFont *font = [lineFragment.attributedString attribute:NSFontAttributeName
98
+ atIndex:intersection.location
99
+ effectiveRange:NULL];
100
+ CGFloat ascent = font.ascender;
101
+ CGFloat textHeight = font.lineHeight;
102
+ CGFloat y = lineBounds.origin.y + startLocation.y - ascent;
103
+
104
+
105
+ CGRect backgroundRect = CGRectMake(x,
106
+ y,
107
+ width,
108
+ textHeight);
109
+
110
+ BOOL isStart = (intersection.location == mention.range.location);
111
+ BOOL isEnd = (NSMaxRange(intersection) == NSMaxRange(mention.range));
112
+ UIRectCorner cornersToRound = 0;
113
+ if (isStart) {
114
+ cornersToRound |= UIRectCornerTopLeft | UIRectCornerBottomLeft;
115
+ }
116
+ if (isEnd) {
117
+ cornersToRound |= UIRectCornerTopRight | UIRectCornerBottomRight;
118
+ }
119
+
120
+ UIBezierPath *linePath = (cornersToRound == 0)
121
+ ? [UIBezierPath bezierPathWithRect:backgroundRect]
122
+ : [UIBezierPath bezierPathWithRoundedRect:backgroundRect
123
+ byRoundingCorners:cornersToRound
124
+ cornerRadii:CGSizeMake(mention.textBackground.borderRadius,
125
+ mention.textBackground.borderRadius)];
126
+
127
+ [mention.textBackground.color setFill];
128
+ [linePath fill];
129
+ }
130
+ }];
131
+ }
132
+
133
+ #pragma mark - helper functions
134
+
135
+ - (CGRect)boundingRect {
136
+ CGRect fragmentTextBounds = CGRectNull;
137
+ for (NSTextLineFragment *lineFragment in self.textLineFragments) {
138
+ if (lineFragment.characterRange.length == 0) {
139
+ continue;
140
+ }
141
+ CGRect lineFragmentBounds = lineFragment.typographicBounds;
142
+ if (CGRectIsNull(fragmentTextBounds)) {
143
+ fragmentTextBounds = lineFragmentBounds;
144
+ } else {
145
+ fragmentTextBounds = CGRectUnion(fragmentTextBounds, lineFragmentBounds);
146
+ }
147
+ }
148
+
149
+ CGFloat marginLeft = _markdownUtils.markdownStyle.blockquoteMarginLeft;
150
+ CGFloat borderWidth = _markdownUtils.markdownStyle.blockquoteBorderWidth;
151
+ CGFloat paddingLeft = _markdownUtils.markdownStyle.blockquotePaddingLeft;
152
+ CGFloat shift = marginLeft + borderWidth + paddingLeft;
153
+
154
+ fragmentTextBounds.origin.x -= (paddingLeft + borderWidth) + shift * (_depth - 1);
155
+ fragmentTextBounds.size.width = borderWidth + shift * (_depth - 1);
156
+
157
+ return fragmentTextBounds;
158
+ }
159
+
160
+ @end