@expensify/react-native-live-markdown 0.1.3
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/LICENSE +20 -0
- package/README.md +139 -0
- package/RNLiveMarkdown.podspec +44 -0
- package/android/build.gradle +143 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/cpp/CMakeLists.txt +23 -0
- package/android/src/main/cpp/MarkdownUtils.cpp +44 -0
- package/android/src/main/cpp/MarkdownUtils.h +41 -0
- package/android/src/main/cpp/OnLoad.cpp +8 -0
- package/android/src/main/java/com/expensify/livemarkdown/LiveMarkdownPackage.java +24 -0
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownBackgroundColorSpan.java +11 -0
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownBlockquoteSpan.java +47 -0
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownBoldSpan.java +10 -0
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownFontFamilySpan.java +39 -0
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownFontSizeSpan.java +9 -0
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownForegroundColorSpan.java +11 -0
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownItalicSpan.java +10 -0
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownLineHeightSpan.java +18 -0
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownSpan.java +7 -0
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownStrikethroughSpan.java +5 -0
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java +186 -0
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorView.java +85 -0
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorViewManager.java +31 -0
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownTextWatcher.java +38 -0
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownUnderlineSpan.java +5 -0
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java +176 -0
- package/android/src/newarch/MarkdownTextInputDecoratorViewManagerSpec.java +24 -0
- package/android/src/oldarch/MarkdownTextInputDecoratorViewManagerSpec.java +10 -0
- package/ios/MarkdownLayoutManager.h +12 -0
- package/ios/MarkdownLayoutManager.mm +35 -0
- package/ios/MarkdownTextInputDecoratorComponentView.h +12 -0
- package/ios/MarkdownTextInputDecoratorComponentView.mm +55 -0
- package/ios/MarkdownTextInputDecoratorView.h +12 -0
- package/ios/MarkdownTextInputDecoratorView.mm +106 -0
- package/ios/MarkdownTextInputDecoratorViewManager.h +8 -0
- package/ios/MarkdownTextInputDecoratorViewManager.mm +23 -0
- package/ios/RCTBackedTextFieldDelegateAdapter+Markdown.h +14 -0
- package/ios/RCTBackedTextFieldDelegateAdapter+Markdown.m +43 -0
- package/ios/RCTBaseTextInputView+Markdown.h +16 -0
- package/ios/RCTBaseTextInputView+Markdown.m +66 -0
- package/ios/RCTMarkdownStyle.h +35 -0
- package/ios/RCTMarkdownStyle.mm +81 -0
- package/ios/RCTMarkdownUtils.h +18 -0
- package/ios/RCTMarkdownUtils.mm +134 -0
- package/ios/RCTTextInputComponentView+Markdown.h +21 -0
- package/ios/RCTTextInputComponentView+Markdown.mm +51 -0
- package/ios/RCTUITextView+Markdown.h +15 -0
- package/ios/RCTUITextView+Markdown.mm +42 -0
- package/lib/commonjs/MarkdownTextInput.js +103 -0
- package/lib/commonjs/MarkdownTextInput.js.map +1 -0
- package/lib/commonjs/MarkdownTextInput.web.js +21 -0
- package/lib/commonjs/MarkdownTextInput.web.js.map +1 -0
- package/lib/commonjs/MarkdownTextInputDecoratorViewNativeComponent.js +10 -0
- package/lib/commonjs/MarkdownTextInputDecoratorViewNativeComponent.js.map +1 -0
- package/lib/commonjs/index.js +14 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/module/MarkdownTextInput.js +96 -0
- package/lib/module/MarkdownTextInput.js.map +1 -0
- package/lib/module/MarkdownTextInput.web.js +15 -0
- package/lib/module/MarkdownTextInput.web.js.map +1 -0
- package/lib/module/MarkdownTextInputDecoratorViewNativeComponent.js +3 -0
- package/lib/module/MarkdownTextInputDecoratorViewNativeComponent.js.map +1 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -0
- package/lib/typescript/src/MarkdownTextInput.d.ts +15 -0
- package/lib/typescript/src/MarkdownTextInput.d.ts.map +1 -0
- package/lib/typescript/src/MarkdownTextInput.web.d.ts +9 -0
- package/lib/typescript/src/MarkdownTextInput.web.d.ts.map +1 -0
- package/lib/typescript/src/MarkdownTextInputDecoratorViewNativeComponent.d.ts +46 -0
- package/lib/typescript/src/MarkdownTextInputDecoratorViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +159 -0
- package/parser/react-native-live-markdown-parser.js +34 -0
- package/src/MarkdownTextInput.tsx +129 -0
- package/src/MarkdownTextInput.web.tsx +24 -0
- package/src/MarkdownTextInputDecoratorViewNativeComponent.ts +48 -0
- package/src/index.tsx +2 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
package com.expensify.livemarkdown;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
|
|
5
|
+
import androidx.annotation.ColorInt;
|
|
6
|
+
import androidx.annotation.NonNull;
|
|
7
|
+
|
|
8
|
+
import com.facebook.react.bridge.ColorPropConverter;
|
|
9
|
+
import com.facebook.react.bridge.Dynamic;
|
|
10
|
+
import com.facebook.react.bridge.JSApplicationCausedNativeException;
|
|
11
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
12
|
+
|
|
13
|
+
import java.util.Objects;
|
|
14
|
+
|
|
15
|
+
public class MarkdownStyle {
|
|
16
|
+
@ColorInt
|
|
17
|
+
private final int mSyntaxColor;
|
|
18
|
+
|
|
19
|
+
@ColorInt
|
|
20
|
+
private final int mLinkColor;
|
|
21
|
+
|
|
22
|
+
private final float mH1FontSize;
|
|
23
|
+
|
|
24
|
+
@ColorInt
|
|
25
|
+
private final int mBlockquoteBorderColor;
|
|
26
|
+
|
|
27
|
+
private final float mBlockquoteBorderWidth;
|
|
28
|
+
|
|
29
|
+
private final float mBlockquoteMarginLeft;
|
|
30
|
+
|
|
31
|
+
private final float mBlockquotePaddingLeft;
|
|
32
|
+
|
|
33
|
+
private final String mCodeFontFamily;
|
|
34
|
+
|
|
35
|
+
@ColorInt
|
|
36
|
+
private final int mCodeColor;
|
|
37
|
+
|
|
38
|
+
@ColorInt
|
|
39
|
+
private final int mCodeBackgroundColor;
|
|
40
|
+
|
|
41
|
+
private final String mPreFontFamily;
|
|
42
|
+
|
|
43
|
+
@ColorInt
|
|
44
|
+
private final int mPreColor;
|
|
45
|
+
|
|
46
|
+
@ColorInt
|
|
47
|
+
private final int mPreBackgroundColor;
|
|
48
|
+
|
|
49
|
+
@ColorInt
|
|
50
|
+
private final int mMentionHereColor;
|
|
51
|
+
|
|
52
|
+
@ColorInt
|
|
53
|
+
private final int mMentionHereBackgroundColor;
|
|
54
|
+
|
|
55
|
+
@ColorInt
|
|
56
|
+
private final int mMentionUserColor;
|
|
57
|
+
|
|
58
|
+
@ColorInt
|
|
59
|
+
private final int mMentionUserBackgroundColor;
|
|
60
|
+
|
|
61
|
+
public MarkdownStyle(@NonNull ReadableMap map, @NonNull Context context) {
|
|
62
|
+
mSyntaxColor = parseColor(map, "syntax", "color", context);
|
|
63
|
+
mLinkColor = parseColor(map, "link", "color", context);
|
|
64
|
+
mH1FontSize = parseFloat(map, "h1", "fontSize");
|
|
65
|
+
mBlockquoteBorderColor = parseColor(map, "blockquote", "borderColor", context);
|
|
66
|
+
mBlockquoteBorderWidth = parseFloat(map, "blockquote", "borderWidth");
|
|
67
|
+
mBlockquoteMarginLeft = parseFloat(map, "blockquote", "marginLeft");
|
|
68
|
+
mBlockquotePaddingLeft = parseFloat(map, "blockquote", "paddingLeft");
|
|
69
|
+
mCodeFontFamily = parseString(map, "code", "fontFamily");
|
|
70
|
+
mCodeColor = parseColor(map, "code", "color", context);
|
|
71
|
+
mCodeBackgroundColor = parseColor(map, "code", "backgroundColor", context);
|
|
72
|
+
mPreFontFamily = parseString(map, "pre", "fontFamily");
|
|
73
|
+
mPreColor = parseColor(map, "pre", "color", context);
|
|
74
|
+
mPreBackgroundColor = parseColor(map, "pre", "backgroundColor", context);
|
|
75
|
+
mMentionHereColor = parseColor(map, "mentionHere", "color", context);
|
|
76
|
+
mMentionHereBackgroundColor = parseColor(map, "mentionHere", "backgroundColor", context);
|
|
77
|
+
mMentionUserColor = parseColor(map, "mentionUser", "color", context);
|
|
78
|
+
mMentionUserBackgroundColor = parseColor(map, "mentionUser", "backgroundColor", context);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private static int parseColor(@NonNull ReadableMap map, @NonNull String key, @NonNull String prop, @NonNull Context context) {
|
|
82
|
+
ReadableMap style = map.getMap(key);
|
|
83
|
+
Objects.requireNonNull(style);
|
|
84
|
+
Dynamic value = style.getDynamic(prop);
|
|
85
|
+
switch (value.getType()) {
|
|
86
|
+
case Number:
|
|
87
|
+
return ColorPropConverter.getColor(value.asDouble(), context);
|
|
88
|
+
case Map:
|
|
89
|
+
return ColorPropConverter.getColor(value.asMap(), context);
|
|
90
|
+
default:
|
|
91
|
+
throw new JSApplicationCausedNativeException("ColorValue: the value must be a number or Object.");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private static float parseFloat(@NonNull ReadableMap map, @NonNull String key, @NonNull String prop) {
|
|
96
|
+
ReadableMap style = map.getMap(key);
|
|
97
|
+
Objects.requireNonNull(style);
|
|
98
|
+
double value = style.getDouble(prop);
|
|
99
|
+
return (float) value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private static String parseString(@NonNull ReadableMap map, @NonNull String key, @NonNull String prop) {
|
|
103
|
+
ReadableMap style = map.getMap(key);
|
|
104
|
+
Objects.requireNonNull(style);
|
|
105
|
+
return style.getString(prop);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@ColorInt
|
|
109
|
+
public int getSyntaxColor() {
|
|
110
|
+
return mSyntaxColor;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@ColorInt
|
|
114
|
+
public int getLinkColor() {
|
|
115
|
+
return mLinkColor;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public float getH1FontSize() {
|
|
119
|
+
return mH1FontSize;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@ColorInt
|
|
123
|
+
public int getBlockquoteBorderColor() {
|
|
124
|
+
return mBlockquoteBorderColor;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public float getBlockquoteBorderWidth() {
|
|
128
|
+
return mBlockquoteBorderWidth;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public float getBlockquoteMarginLeft() {
|
|
132
|
+
return mBlockquoteMarginLeft;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public float getBlockquotePaddingLeft() {
|
|
136
|
+
return mBlockquotePaddingLeft;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public String getCodeFontFamily() {
|
|
140
|
+
return mCodeFontFamily;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@ColorInt
|
|
144
|
+
public int getCodeColor() {
|
|
145
|
+
return mCodeColor;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@ColorInt
|
|
149
|
+
public int getCodeBackgroundColor() {
|
|
150
|
+
return mCodeBackgroundColor;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public String getPreFontFamily() {
|
|
154
|
+
return mPreFontFamily;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@ColorInt
|
|
158
|
+
public int getPreColor() {
|
|
159
|
+
return mPreColor;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@ColorInt
|
|
163
|
+
public int getPreBackgroundColor() {
|
|
164
|
+
return mPreBackgroundColor;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@ColorInt
|
|
168
|
+
public int getMentionHereColor() {
|
|
169
|
+
return mMentionHereColor;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@ColorInt
|
|
173
|
+
public int getMentionHereBackgroundColor() {
|
|
174
|
+
return mMentionHereBackgroundColor;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@ColorInt
|
|
178
|
+
public int getMentionUserColor() {
|
|
179
|
+
return mMentionUserColor;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@ColorInt
|
|
183
|
+
public int getMentionUserBackgroundColor() {
|
|
184
|
+
return mMentionUserBackgroundColor;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
package com.expensify.livemarkdown;
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.Nullable;
|
|
4
|
+
|
|
5
|
+
import android.content.Context;
|
|
6
|
+
import android.content.res.AssetManager;
|
|
7
|
+
import android.text.TextWatcher;
|
|
8
|
+
import android.util.AttributeSet;
|
|
9
|
+
|
|
10
|
+
import android.view.View;
|
|
11
|
+
import android.view.ViewGroup;
|
|
12
|
+
import android.view.ViewParent;
|
|
13
|
+
|
|
14
|
+
import com.facebook.react.views.textinput.ReactEditText;
|
|
15
|
+
|
|
16
|
+
public class MarkdownTextInputDecoratorView extends View {
|
|
17
|
+
|
|
18
|
+
public MarkdownTextInputDecoratorView(Context context) {
|
|
19
|
+
super(context);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public MarkdownTextInputDecoratorView(Context context, @Nullable AttributeSet attrs) {
|
|
23
|
+
super(context, attrs);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public MarkdownTextInputDecoratorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
|
27
|
+
super(context, attrs, defStyleAttr);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private MarkdownStyle mMarkdownStyle;
|
|
31
|
+
|
|
32
|
+
private MarkdownUtils mMarkdownUtils;
|
|
33
|
+
|
|
34
|
+
private ReactEditText mReactEditText;
|
|
35
|
+
|
|
36
|
+
private TextWatcher mTextWatcher;
|
|
37
|
+
|
|
38
|
+
@Override
|
|
39
|
+
protected void onAttachedToWindow() {
|
|
40
|
+
super.onAttachedToWindow();
|
|
41
|
+
|
|
42
|
+
View previousSibling = null;
|
|
43
|
+
final ViewParent parent = this.getParent();
|
|
44
|
+
if (parent instanceof ViewGroup) {
|
|
45
|
+
final ViewGroup viewGroup = (ViewGroup) parent;
|
|
46
|
+
for (int i = 1; i < viewGroup.getChildCount(); i++) {
|
|
47
|
+
if (viewGroup.getChildAt(i) == this) {
|
|
48
|
+
previousSibling = viewGroup.getChildAt(i - 1);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (previousSibling instanceof ReactEditText) {
|
|
55
|
+
AssetManager assetManager = getContext().getAssets();
|
|
56
|
+
MarkdownUtils.maybeInitializeRuntime(assetManager);
|
|
57
|
+
mMarkdownUtils = new MarkdownUtils(assetManager);
|
|
58
|
+
mMarkdownUtils.setMarkdownStyle(mMarkdownStyle);
|
|
59
|
+
mReactEditText = (ReactEditText) previousSibling;
|
|
60
|
+
mTextWatcher = new MarkdownTextWatcher(mMarkdownUtils);
|
|
61
|
+
mReactEditText.addTextChangedListener(mTextWatcher);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Override
|
|
66
|
+
protected void onDetachedFromWindow() {
|
|
67
|
+
super.onDetachedFromWindow();
|
|
68
|
+
if (mReactEditText != null) {
|
|
69
|
+
mReactEditText.removeTextChangedListener(mTextWatcher);
|
|
70
|
+
mReactEditText = null;
|
|
71
|
+
mTextWatcher = null;
|
|
72
|
+
mMarkdownUtils = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
protected void setMarkdownStyle(MarkdownStyle markdownStyle) {
|
|
77
|
+
mMarkdownStyle = markdownStyle;
|
|
78
|
+
if (mMarkdownUtils != null) {
|
|
79
|
+
mMarkdownUtils.setMarkdownStyle(mMarkdownStyle);
|
|
80
|
+
}
|
|
81
|
+
if (mReactEditText != null) {
|
|
82
|
+
mReactEditText.setText(mReactEditText.getText()); // trigger update
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
package/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorViewManager.java
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.expensify.livemarkdown;
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.NonNull;
|
|
4
|
+
|
|
5
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
6
|
+
import com.facebook.react.module.annotations.ReactModule;
|
|
7
|
+
import com.facebook.react.uimanager.ThemedReactContext;
|
|
8
|
+
import com.facebook.react.uimanager.annotations.ReactProp;
|
|
9
|
+
|
|
10
|
+
@ReactModule(name = MarkdownTextInputDecoratorViewManager.NAME)
|
|
11
|
+
public class MarkdownTextInputDecoratorViewManager extends MarkdownTextInputDecoratorViewManagerSpec<MarkdownTextInputDecoratorView> {
|
|
12
|
+
|
|
13
|
+
public static final String NAME = "MarkdownTextInputDecoratorView";
|
|
14
|
+
|
|
15
|
+
@Override
|
|
16
|
+
public String getName() {
|
|
17
|
+
return NAME;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Override
|
|
21
|
+
public MarkdownTextInputDecoratorView createViewInstance(ThemedReactContext context) {
|
|
22
|
+
return new MarkdownTextInputDecoratorView(context);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Override
|
|
26
|
+
@ReactProp(name = "markdownStyle")
|
|
27
|
+
public void setMarkdownStyle(@NonNull MarkdownTextInputDecoratorView view, @NonNull ReadableMap value) {
|
|
28
|
+
MarkdownStyle markdownStyle = new MarkdownStyle(value, view.getContext());
|
|
29
|
+
view.setMarkdownStyle(markdownStyle);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package com.expensify.livemarkdown;
|
|
2
|
+
|
|
3
|
+
import android.text.Editable;
|
|
4
|
+
import android.text.SpannableStringBuilder;
|
|
5
|
+
import android.text.TextWatcher;
|
|
6
|
+
|
|
7
|
+
import androidx.annotation.NonNull;
|
|
8
|
+
|
|
9
|
+
public class MarkdownTextWatcher implements TextWatcher {
|
|
10
|
+
private final MarkdownUtils mMarkdownUtils;
|
|
11
|
+
|
|
12
|
+
private boolean mShouldSkip = false;
|
|
13
|
+
|
|
14
|
+
public MarkdownTextWatcher(@NonNull MarkdownUtils markdownUtils) {
|
|
15
|
+
mMarkdownUtils = markdownUtils;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Override
|
|
19
|
+
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
|
20
|
+
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Override
|
|
24
|
+
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
|
25
|
+
if (mShouldSkip) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (s instanceof SpannableStringBuilder) {
|
|
29
|
+
mMarkdownUtils.applyMarkdownFormatting((SpannableStringBuilder) s);
|
|
30
|
+
mShouldSkip = true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Override
|
|
35
|
+
public void afterTextChanged(Editable editable) {
|
|
36
|
+
mShouldSkip = false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
package com.expensify.livemarkdown;
|
|
2
|
+
|
|
3
|
+
import static com.facebook.infer.annotation.ThreadConfined.UI;
|
|
4
|
+
|
|
5
|
+
import android.content.res.AssetManager;
|
|
6
|
+
import android.text.SpannableStringBuilder;
|
|
7
|
+
import android.text.Spanned;
|
|
8
|
+
|
|
9
|
+
import androidx.annotation.NonNull;
|
|
10
|
+
|
|
11
|
+
import com.facebook.infer.annotation.Assertions;
|
|
12
|
+
import com.facebook.infer.annotation.ThreadConfined;
|
|
13
|
+
import com.facebook.react.bridge.UiThreadUtil;
|
|
14
|
+
import com.facebook.react.views.text.CustomLineHeightSpan;
|
|
15
|
+
import com.facebook.soloader.SoLoader;
|
|
16
|
+
|
|
17
|
+
import org.json.JSONArray;
|
|
18
|
+
import org.json.JSONException;
|
|
19
|
+
|
|
20
|
+
import java.io.IOException;
|
|
21
|
+
import java.io.InputStream;
|
|
22
|
+
import java.util.Objects;
|
|
23
|
+
|
|
24
|
+
public class MarkdownUtils {
|
|
25
|
+
static {
|
|
26
|
+
SoLoader.loadLibrary("livemarkdown");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private static boolean IS_RUNTIME_INITIALIZED = false;
|
|
30
|
+
|
|
31
|
+
@ThreadConfined(UI)
|
|
32
|
+
public static void maybeInitializeRuntime(AssetManager assetManager) {
|
|
33
|
+
UiThreadUtil.assertOnUiThread();
|
|
34
|
+
if (IS_RUNTIME_INITIALIZED) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
InputStream inputStream = assetManager.open("react-native-live-markdown-parser.js");
|
|
39
|
+
byte[] buffer = new byte[inputStream.available()];
|
|
40
|
+
inputStream.read(buffer);
|
|
41
|
+
inputStream.close();
|
|
42
|
+
String code = new String(buffer);
|
|
43
|
+
nativeInitializeRuntime(code);
|
|
44
|
+
IS_RUNTIME_INITIALIZED = true;
|
|
45
|
+
} catch (IOException e) {
|
|
46
|
+
throw new RuntimeException("Failed to initialize Markdown runtime");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private static native void nativeInitializeRuntime(String code);
|
|
51
|
+
|
|
52
|
+
@ThreadConfined(UI)
|
|
53
|
+
private static String parseMarkdown(String input) {
|
|
54
|
+
UiThreadUtil.assertOnUiThread();
|
|
55
|
+
return nativeParseMarkdown(input);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private static native String nativeParseMarkdown(String input);
|
|
59
|
+
|
|
60
|
+
public MarkdownUtils(@NonNull AssetManager assetManager) {
|
|
61
|
+
mAssetManager = assetManager;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private final @NonNull AssetManager mAssetManager;
|
|
65
|
+
|
|
66
|
+
private String mPrevInput;
|
|
67
|
+
|
|
68
|
+
private String mPrevOutput;
|
|
69
|
+
|
|
70
|
+
private MarkdownStyle mMarkdownStyle;
|
|
71
|
+
|
|
72
|
+
public void setMarkdownStyle(@NonNull MarkdownStyle markdownStyle) {
|
|
73
|
+
mMarkdownStyle = markdownStyle;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public void applyMarkdownFormatting(SpannableStringBuilder ssb) {
|
|
77
|
+
Objects.requireNonNull(mMarkdownStyle, "mMarkdownStyle is null");
|
|
78
|
+
|
|
79
|
+
removeSpans(ssb);
|
|
80
|
+
|
|
81
|
+
String input = ssb.toString();
|
|
82
|
+
String output;
|
|
83
|
+
if (input.equals(mPrevInput)) {
|
|
84
|
+
output = mPrevOutput;
|
|
85
|
+
} else {
|
|
86
|
+
output = parseMarkdown(input);
|
|
87
|
+
mPrevInput = input;
|
|
88
|
+
mPrevOutput = output;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
JSONArray ranges = new JSONArray(output);
|
|
93
|
+
for (int i = 0; i < ranges.length(); i++) {
|
|
94
|
+
JSONArray range = ranges.getJSONArray(i);
|
|
95
|
+
String type = range.getString(0);
|
|
96
|
+
int start = range.getInt(1);
|
|
97
|
+
int end = start + range.getInt(2);
|
|
98
|
+
applyRange(ssb, type, start, end);
|
|
99
|
+
}
|
|
100
|
+
} catch (JSONException e) {
|
|
101
|
+
// Do nothing
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private void applyRange(SpannableStringBuilder ssb, String type, int start, int end) {
|
|
106
|
+
switch (type) {
|
|
107
|
+
case "bold":
|
|
108
|
+
setSpan(ssb, new MarkdownBoldSpan(), start, end);
|
|
109
|
+
break;
|
|
110
|
+
case "italic":
|
|
111
|
+
setSpan(ssb, new MarkdownItalicSpan(), start, end);
|
|
112
|
+
break;
|
|
113
|
+
case "strikethrough":
|
|
114
|
+
setSpan(ssb, new MarkdownStrikethroughSpan(), start, end);
|
|
115
|
+
break;
|
|
116
|
+
case "mention-here":
|
|
117
|
+
setSpan(ssb, new MarkdownForegroundColorSpan(mMarkdownStyle.getMentionHereColor()), start, end);
|
|
118
|
+
setSpan(ssb, new MarkdownBackgroundColorSpan(mMarkdownStyle.getMentionHereBackgroundColor()), start, end);
|
|
119
|
+
break;
|
|
120
|
+
case "mention-user":
|
|
121
|
+
// TODO: change mention color when it mentions current user
|
|
122
|
+
setSpan(ssb, new MarkdownForegroundColorSpan(mMarkdownStyle.getMentionUserColor()), start, end);
|
|
123
|
+
setSpan(ssb, new MarkdownBackgroundColorSpan(mMarkdownStyle.getMentionUserBackgroundColor()), start, end);
|
|
124
|
+
break;
|
|
125
|
+
case "syntax":
|
|
126
|
+
setSpan(ssb, new MarkdownForegroundColorSpan(mMarkdownStyle.getSyntaxColor()), start, end);
|
|
127
|
+
break;
|
|
128
|
+
case "link":
|
|
129
|
+
setSpan(ssb, new MarkdownUnderlineSpan(), start, end);
|
|
130
|
+
setSpan(ssb, new MarkdownForegroundColorSpan(mMarkdownStyle.getLinkColor()), start, end);
|
|
131
|
+
break;
|
|
132
|
+
case "code":
|
|
133
|
+
setSpan(ssb, new MarkdownFontFamilySpan(mMarkdownStyle.getCodeFontFamily(), mAssetManager), start, end);
|
|
134
|
+
setSpan(ssb, new MarkdownForegroundColorSpan(mMarkdownStyle.getCodeColor()), start, end);
|
|
135
|
+
setSpan(ssb, new MarkdownBackgroundColorSpan(mMarkdownStyle.getCodeBackgroundColor()), start, end);
|
|
136
|
+
break;
|
|
137
|
+
case "pre":
|
|
138
|
+
setSpan(ssb, new MarkdownFontFamilySpan(mMarkdownStyle.getPreFontFamily(), mAssetManager), start, end);
|
|
139
|
+
setSpan(ssb, new MarkdownForegroundColorSpan(mMarkdownStyle.getPreColor()), start, end);
|
|
140
|
+
setSpan(ssb, new MarkdownBackgroundColorSpan(mMarkdownStyle.getPreBackgroundColor()), start, end);
|
|
141
|
+
break;
|
|
142
|
+
case "h1":
|
|
143
|
+
setSpan(ssb, new MarkdownBoldSpan(), start, end);
|
|
144
|
+
CustomLineHeightSpan[] spans = ssb.getSpans(0, ssb.length(), CustomLineHeightSpan.class);
|
|
145
|
+
if (spans.length >= 1) {
|
|
146
|
+
int lineHeight = spans[0].getLineHeight();
|
|
147
|
+
setSpan(ssb, new MarkdownLineHeightSpan(lineHeight * 1.5f), start, end);
|
|
148
|
+
}
|
|
149
|
+
// NOTE: size span must be set after line height span to avoid height jumps
|
|
150
|
+
setSpan(ssb, new MarkdownFontSizeSpan(mMarkdownStyle.getH1FontSize()), start, end);
|
|
151
|
+
break;
|
|
152
|
+
case "blockquote":
|
|
153
|
+
MarkdownBlockquoteSpan span = new MarkdownBlockquoteSpan(
|
|
154
|
+
mMarkdownStyle.getBlockquoteBorderColor(),
|
|
155
|
+
mMarkdownStyle.getBlockquoteBorderWidth(),
|
|
156
|
+
mMarkdownStyle.getBlockquoteMarginLeft(),
|
|
157
|
+
mMarkdownStyle.getBlockquotePaddingLeft());
|
|
158
|
+
setSpan(ssb, span, start, end);
|
|
159
|
+
break;
|
|
160
|
+
default:
|
|
161
|
+
throw new IllegalStateException("Unsupported type: " + type);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private void setSpan(SpannableStringBuilder ssb, MarkdownSpan span, int start, int end) {
|
|
166
|
+
ssb.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private void removeSpans(SpannableStringBuilder ssb) {
|
|
170
|
+
// We shouldn't use `removeSpans()` because it also removes SpellcheckSpan, SuggestionSpan etc.
|
|
171
|
+
MarkdownSpan[] spans = ssb.getSpans(0, ssb.length(), MarkdownSpan.class);
|
|
172
|
+
for (MarkdownSpan span : spans) {
|
|
173
|
+
ssb.removeSpan(span);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package com.expensify.livemarkdown;
|
|
2
|
+
|
|
3
|
+
import android.view.View;
|
|
4
|
+
|
|
5
|
+
import androidx.annotation.Nullable;
|
|
6
|
+
|
|
7
|
+
import com.facebook.react.uimanager.SimpleViewManager;
|
|
8
|
+
import com.facebook.react.uimanager.ViewManagerDelegate;
|
|
9
|
+
import com.facebook.react.viewmanagers.MarkdownTextInputDecoratorViewManagerDelegate;
|
|
10
|
+
import com.facebook.react.viewmanagers.MarkdownTextInputDecoratorViewManagerInterface;
|
|
11
|
+
|
|
12
|
+
public abstract class MarkdownTextInputDecoratorViewManagerSpec<T extends View> extends SimpleViewManager<T> implements MarkdownTextInputDecoratorViewManagerInterface<T> {
|
|
13
|
+
private final ViewManagerDelegate<T> mDelegate;
|
|
14
|
+
|
|
15
|
+
public MarkdownTextInputDecoratorViewManagerSpec() {
|
|
16
|
+
mDelegate = new MarkdownTextInputDecoratorViewManagerDelegate(this);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@Nullable
|
|
20
|
+
@Override
|
|
21
|
+
protected ViewManagerDelegate<T> getDelegate() {
|
|
22
|
+
return mDelegate;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
package com.expensify.livemarkdown;
|
|
2
|
+
|
|
3
|
+
import android.view.View;
|
|
4
|
+
|
|
5
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
6
|
+
import com.facebook.react.uimanager.SimpleViewManager;
|
|
7
|
+
|
|
8
|
+
public abstract class MarkdownTextInputDecoratorViewManagerSpec<T extends View> extends SimpleViewManager<T> {
|
|
9
|
+
public abstract void setMarkdownStyle(T view, ReadableMap value);
|
|
10
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#import <RNLiveMarkdown/MarkdownLayoutManager.h>
|
|
2
|
+
|
|
3
|
+
@implementation MarkdownLayoutManager
|
|
4
|
+
|
|
5
|
+
- (void)drawBackgroundForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin {
|
|
6
|
+
[super drawBackgroundForGlyphRange:glyphsToShow atPoint:origin];
|
|
7
|
+
|
|
8
|
+
[self enumerateLineFragmentsForGlyphRange:glyphsToShow usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer * _Nonnull textContainer, NSRange glyphRange, BOOL * _Nonnull stop) {
|
|
9
|
+
__block BOOL isBlockquote = NO;
|
|
10
|
+
RCTMarkdownUtils *markdownUtils = [self valueForKey:@"markdownUtils"];
|
|
11
|
+
[markdownUtils.blockquoteRanges enumerateObjectsUsingBlock:^(NSValue *item, NSUInteger idx, BOOL * _Nonnull stop) {
|
|
12
|
+
NSRange range = [item rangeValue];
|
|
13
|
+
NSUInteger start = range.location;
|
|
14
|
+
NSUInteger end = start + range.length;
|
|
15
|
+
NSUInteger location = glyphRange.location;
|
|
16
|
+
if (location >= start && location < end) {
|
|
17
|
+
isBlockquote = YES;
|
|
18
|
+
*stop = YES;
|
|
19
|
+
}
|
|
20
|
+
}];
|
|
21
|
+
if (isBlockquote) {
|
|
22
|
+
CGFloat paddingLeft = markdownUtils.backedTextInputView.textContainerInset.left;
|
|
23
|
+
CGFloat paddingTop = markdownUtils.backedTextInputView.textContainerInset.top;
|
|
24
|
+
CGFloat x = paddingLeft + markdownUtils.markdownStyle.blockquoteMarginLeft;
|
|
25
|
+
CGFloat y = paddingTop + rect.origin.y;
|
|
26
|
+
CGFloat width = markdownUtils.markdownStyle.blockquoteBorderWidth;
|
|
27
|
+
CGFloat height = rect.size.height;
|
|
28
|
+
CGRect lineRect = CGRectMake(x, y, width, height);
|
|
29
|
+
[markdownUtils.markdownStyle.blockquoteBorderColor setFill];
|
|
30
|
+
UIRectFill(lineRect);
|
|
31
|
+
}
|
|
32
|
+
}];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// This guard prevent this file to be compiled in the old architecture.
|
|
2
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
3
|
+
#import <React/RCTViewComponentView.h>
|
|
4
|
+
|
|
5
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
6
|
+
|
|
7
|
+
@interface MarkdownTextInputDecoratorComponentView : RCTViewComponentView
|
|
8
|
+
@end
|
|
9
|
+
|
|
10
|
+
NS_ASSUME_NONNULL_END
|
|
11
|
+
|
|
12
|
+
#endif /* RCT_NEW_ARCH_ENABLED */
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// This guard prevent this file to be compiled in the old architecture.
|
|
2
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
3
|
+
#import <react/renderer/components/RNLiveMarkdownSpec/ComponentDescriptors.h>
|
|
4
|
+
#import <react/renderer/components/RNLiveMarkdownSpec/Props.h>
|
|
5
|
+
|
|
6
|
+
#import <RNLiveMarkdown/MarkdownTextInputDecoratorComponentView.h>
|
|
7
|
+
#import <RNLiveMarkdown/MarkdownTextInputDecoratorView.h>
|
|
8
|
+
#import <RNLiveMarkdown/RCTMarkdownStyle.h>
|
|
9
|
+
|
|
10
|
+
#import "RCTFabricComponentsPlugins.h"
|
|
11
|
+
|
|
12
|
+
using namespace facebook::react;
|
|
13
|
+
|
|
14
|
+
@implementation MarkdownTextInputDecoratorComponentView {
|
|
15
|
+
MarkdownTextInputDecoratorView *_view;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
19
|
+
{
|
|
20
|
+
return concreteComponentDescriptorProvider<MarkdownTextInputDecoratorViewComponentDescriptor>();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
24
|
+
{
|
|
25
|
+
if (self = [super initWithFrame:frame]) {
|
|
26
|
+
static const auto defaultProps = std::make_shared<const MarkdownTextInputDecoratorViewProps>();
|
|
27
|
+
_props = defaultProps;
|
|
28
|
+
|
|
29
|
+
_view = [[MarkdownTextInputDecoratorView alloc] init];
|
|
30
|
+
|
|
31
|
+
self.contentView = _view;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return self;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
38
|
+
{
|
|
39
|
+
const auto &oldViewProps = *std::static_pointer_cast<MarkdownTextInputDecoratorViewProps const>(_props);
|
|
40
|
+
const auto &newViewProps = *std::static_pointer_cast<MarkdownTextInputDecoratorViewProps const>(props);
|
|
41
|
+
|
|
42
|
+
// TODO: if (oldViewProps.markdownStyle != newViewProps.markdownStyle)
|
|
43
|
+
RCTMarkdownStyle *markdownStyle = [[RCTMarkdownStyle alloc] initWithStruct:newViewProps.markdownStyle];
|
|
44
|
+
[_view setMarkdownStyle:markdownStyle];
|
|
45
|
+
|
|
46
|
+
[super updateProps:props oldProps:oldProps];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Class<RCTComponentViewProtocol> MarkdownTextInputDecoratorViewCls(void)
|
|
50
|
+
{
|
|
51
|
+
return MarkdownTextInputDecoratorComponentView.class;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@end
|
|
55
|
+
#endif
|