@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.
- package/README.md +5 -5
- package/RNLiveMarkdown.podspec +7 -20
- package/android/build.gradle +4 -16
- package/android/src/main/cpp/CMakeLists.txt +5 -23
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java +3 -3
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java +23 -0
- package/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java +94 -0
- package/apple/MarkdownFormatter.h +2 -0
- package/apple/MarkdownFormatter.mm +36 -6
- package/apple/MarkdownParser.mm +0 -4
- package/apple/MarkdownTextInputDecoratorComponentView.mm +17 -2
- package/apple/{BlockquoteTextLayoutFragment.h → MarkdownTextLayoutFragment.h} +4 -1
- package/apple/MarkdownTextLayoutFragment.mm +160 -0
- package/apple/MarkdownTextLayoutManagerDelegate.mm +22 -4
- package/apple/RCTMarkdownStyle.h +3 -0
- package/apple/RCTMarkdownStyle.mm +3 -0
- package/apple/RCTMarkdownTextBackground.h +9 -0
- package/apple/RCTMarkdownTextBackground.mm +4 -0
- package/apple/RCTMarkdownTextBackgroundWithRange.h +10 -0
- package/apple/RCTMarkdownTextBackgroundWithRange.mm +5 -0
- package/cpp/MarkdownGlobal.h +0 -4
- package/cpp/RuntimeDecorator.cpp +1 -6
- package/lib/commonjs/MarkdownTextInput.js +6 -4
- package/lib/commonjs/MarkdownTextInput.js.map +1 -1
- package/lib/commonjs/parseExpensiMark.js.map +1 -1
- package/lib/commonjs/styleUtils.js +6 -3
- package/lib/commonjs/styleUtils.js.map +1 -1
- package/lib/module/MarkdownTextInput.js +6 -4
- package/lib/module/MarkdownTextInput.js.map +1 -1
- package/lib/module/parseExpensiMark.js.map +1 -1
- package/lib/module/styleUtils.js +6 -3
- package/lib/module/styleUtils.js.map +1 -1
- package/lib/typescript/src/MarkdownTextInput.d.ts +3 -2
- package/lib/typescript/src/MarkdownTextInput.d.ts.map +1 -1
- package/lib/typescript/src/commonTypes.d.ts +1 -55
- package/lib/typescript/src/commonTypes.d.ts.map +1 -1
- package/lib/typescript/src/parseExpensiMark.d.ts.map +1 -1
- package/lib/typescript/src/styleUtils.d.ts.map +1 -1
- package/package.json +1 -8
- package/src/MarkdownTextInput.tsx +7 -6
- package/src/commonTypes.ts +1 -75
- package/src/parseExpensiMark.ts +2 -1
- package/src/styleUtils.ts +3 -0
- 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-
|
|
19
|
-
npm install @expensify/react-native-live-markdown react-native-
|
|
20
|
-
npx expo install @expensify/react-native-live-markdown react-native-
|
|
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-
|
|
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-
|
|
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
|
|
package/RNLiveMarkdown.podspec
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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)/#{
|
|
46
|
-
"\"$(PODS_ROOT)/#{
|
|
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
|
|
package/android/build.gradle
CHANGED
|
@@ -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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
[
|
|
94
|
-
|
|
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
|
-
[
|
|
98
|
-
|
|
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
|
-
[
|
|
101
|
-
|
|
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;
|
package/apple/MarkdownParser.mm
CHANGED
|
@@ -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
|
|
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
|