react-native-markdown-native 1.0.0
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 +21 -0
- package/MarkdownNative.podspec +19 -0
- package/README.md +109 -0
- package/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.9/executionHistory/executionHistory.lock +0 -0
- package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.9/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/build.gradle +36 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/markdownnative/MarkdownPackage.java +25 -0
- package/android/src/main/java/com/markdownnative/MarkdownView.java +516 -0
- package/android/src/main/java/com/markdownnative/MarkdownViewManager.java +107 -0
- package/ios/MarkdownView.swift +771 -0
- package/ios/MarkdownViewManager.m +27 -0
- package/ios/MarkdownViewManager.swift +20 -0
- package/lib/commonjs/index.js +97 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types.js +154 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/index.d.ts +26 -0
- package/lib/index.js +97 -0
- package/lib/module/index.js +80 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/types.js +153 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/index.d.ts +30 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +80 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/package.json +92 -0
- package/react-native.config.js +11 -0
- package/src/index.tsx +117 -0
- package/src/types.ts +205 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
package com.markdownnative;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.graphics.Color;
|
|
5
|
+
import android.graphics.Typeface;
|
|
6
|
+
import android.text.method.ArrowKeyMovementMethod;
|
|
7
|
+
import android.text.method.LinkMovementMethod;
|
|
8
|
+
import android.text.style.ForegroundColorSpan;
|
|
9
|
+
import android.widget.TextView;
|
|
10
|
+
|
|
11
|
+
import com.facebook.react.bridge.Arguments;
|
|
12
|
+
import com.facebook.react.bridge.ReactContext;
|
|
13
|
+
import com.facebook.react.bridge.WritableMap;
|
|
14
|
+
import com.facebook.react.uimanager.PixelUtil;
|
|
15
|
+
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
|
16
|
+
|
|
17
|
+
import android.text.Layout;
|
|
18
|
+
import android.text.Spannable;
|
|
19
|
+
import android.text.style.ClickableSpan;
|
|
20
|
+
import android.view.MotionEvent;
|
|
21
|
+
import android.graphics.PorterDuff;
|
|
22
|
+
import android.graphics.drawable.Drawable;
|
|
23
|
+
import android.os.Build;
|
|
24
|
+
|
|
25
|
+
import io.noties.markwon.Markwon;
|
|
26
|
+
import io.noties.markwon.MarkwonSpansFactory;
|
|
27
|
+
import io.noties.markwon.core.MarkwonTheme;
|
|
28
|
+
import io.noties.markwon.html.HtmlPlugin;
|
|
29
|
+
import com.facebook.react.views.text.ReactFontManager;
|
|
30
|
+
|
|
31
|
+
public class MarkdownView extends TextView {
|
|
32
|
+
|
|
33
|
+
private Markwon markwon;
|
|
34
|
+
private String markdownText = "";
|
|
35
|
+
private Context context;
|
|
36
|
+
|
|
37
|
+
// Styling props
|
|
38
|
+
private int linkColor = Color.parseColor("#0066CC");
|
|
39
|
+
private float lineSpacing = 0f;
|
|
40
|
+
private float paragraphSpacing = 0f;
|
|
41
|
+
private float bulletIndent = 0f;
|
|
42
|
+
private float listLeftInset = 0f;
|
|
43
|
+
private float listSpacingBefore = 0f;
|
|
44
|
+
private float listSpacingAfter = 0f;
|
|
45
|
+
|
|
46
|
+
// Rules-based styling system
|
|
47
|
+
private String markdownRulesJson = "";
|
|
48
|
+
private org.json.JSONObject parsedRules = null;
|
|
49
|
+
|
|
50
|
+
public MarkdownView(Context context) {
|
|
51
|
+
super(context);
|
|
52
|
+
this.context = context;
|
|
53
|
+
this.setTextIsSelectable(true);
|
|
54
|
+
this.setFocusable(true);
|
|
55
|
+
this.setFocusableInTouchMode(true);
|
|
56
|
+
rebuildMarkwon();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private void rebuildMarkwon() {
|
|
60
|
+
// Get values from rules if available, otherwise use defaults
|
|
61
|
+
final int finalBulletIndent = parsedRules != null ?
|
|
62
|
+
(int) PixelUtil.toPixelFromDIP(getRuleFloatValue("listItem", "bulletIndent", bulletIndent > 0 ? bulletIndent : 16f)) :
|
|
63
|
+
bulletIndent > 0 ? (int) PixelUtil.toPixelFromDIP(bulletIndent) : (int) PixelUtil.toPixelFromDIP(16);
|
|
64
|
+
|
|
65
|
+
final int finalListLeftInset = parsedRules != null ?
|
|
66
|
+
(int) PixelUtil.toPixelFromDIP(getRuleFloatValue("listItem", "listLeftInset", listLeftInset)) :
|
|
67
|
+
(int) PixelUtil.toPixelFromDIP(listLeftInset);
|
|
68
|
+
|
|
69
|
+
final int finalLinkColor = parsedRules != null ?
|
|
70
|
+
getRuleColorValue("link", "color", linkColor) :
|
|
71
|
+
linkColor;
|
|
72
|
+
|
|
73
|
+
// Get heading size multipliers from rules or use defaults
|
|
74
|
+
final float[] headingMultipliers = new float[6];
|
|
75
|
+
for (int i = 0; i < 6; i++) {
|
|
76
|
+
String headingType = "heading" + (i + 1);
|
|
77
|
+
float defaultSize;
|
|
78
|
+
switch (i) {
|
|
79
|
+
case 0: defaultSize = 32f; break; // H1
|
|
80
|
+
case 1: defaultSize = 28f; break; // H2
|
|
81
|
+
case 2: defaultSize = 24f; break; // H3
|
|
82
|
+
case 3: defaultSize = 20f; break; // H4
|
|
83
|
+
case 4: defaultSize = 18f; break; // H5
|
|
84
|
+
case 5: defaultSize = 16f; break; // H6
|
|
85
|
+
default: defaultSize = 16f;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (parsedRules != null) {
|
|
89
|
+
float ruleSize = getRuleFloatValue(headingType, "fontSize", defaultSize);
|
|
90
|
+
// Convert to multiplier based on base font size (16)
|
|
91
|
+
headingMultipliers[i] = ruleSize / 16f;
|
|
92
|
+
} else {
|
|
93
|
+
// Use default multipliers
|
|
94
|
+
headingMultipliers[i] = defaultSize / 16f;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.markwon = Markwon.builder(context)
|
|
99
|
+
.usePlugin(HtmlPlugin.create())
|
|
100
|
+
.usePlugin(new io.noties.markwon.AbstractMarkwonPlugin() {
|
|
101
|
+
@Override
|
|
102
|
+
public void configureTheme(@androidx.annotation.NonNull MarkwonTheme.Builder builder) {
|
|
103
|
+
builder
|
|
104
|
+
.headingBreakHeight(0)
|
|
105
|
+
.bulletWidth(finalBulletIndent)
|
|
106
|
+
.listItemColor(markdownColor)
|
|
107
|
+
.linkColor(finalLinkColor)
|
|
108
|
+
.headingTextSizeMultipliers(headingMultipliers);
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
.build();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
private int lastDispatchedHeight = -1;
|
|
116
|
+
|
|
117
|
+
@Override
|
|
118
|
+
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
119
|
+
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
|
120
|
+
|
|
121
|
+
android.text.Layout layout = getLayout();
|
|
122
|
+
if (layout != null) {
|
|
123
|
+
int textHeight = layout.getHeight() + getPaddingTop() + getPaddingBottom();
|
|
124
|
+
int width = getMeasuredWidth();
|
|
125
|
+
|
|
126
|
+
if (width > 0 && textHeight > 0 && Math.abs(textHeight - lastDispatchedHeight) > 1) {
|
|
127
|
+
lastDispatchedHeight = textHeight;
|
|
128
|
+
|
|
129
|
+
WritableMap map = Arguments.createMap();
|
|
130
|
+
map.putDouble("width", PixelUtil.toDIPFromPixel(width));
|
|
131
|
+
map.putDouble("height", PixelUtil.toDIPFromPixel(textHeight));
|
|
132
|
+
|
|
133
|
+
Context ctx = getContext();
|
|
134
|
+
if (ctx instanceof ReactContext) {
|
|
135
|
+
((ReactContext) ctx)
|
|
136
|
+
.getJSModule(RCTEventEmitter.class)
|
|
137
|
+
.receiveEvent(getId(), "onNativeSizeChange", map);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@Override
|
|
144
|
+
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
|
145
|
+
super.onLayout(changed, left, top, right, bottom);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
private boolean isInLongPress = false;
|
|
150
|
+
|
|
151
|
+
@Override
|
|
152
|
+
public boolean onTouchEvent(MotionEvent event) {
|
|
153
|
+
// If text is selectable, we need to handle touch events carefully
|
|
154
|
+
// We only want to block parent scrolling when:
|
|
155
|
+
// 1. User is long-pressing (text selection is starting)
|
|
156
|
+
// 2. User is dragging selection handles (text is already selected)
|
|
157
|
+
// Otherwise, allow normal scrolling
|
|
158
|
+
|
|
159
|
+
if (isTextSelectable()) {
|
|
160
|
+
int action = event.getAction();
|
|
161
|
+
|
|
162
|
+
switch (action) {
|
|
163
|
+
case MotionEvent.ACTION_DOWN:
|
|
164
|
+
// Don't block on ACTION_DOWN - allow scrolling to work normally
|
|
165
|
+
// We'll only block if long press is detected or text is selected
|
|
166
|
+
isInLongPress = false;
|
|
167
|
+
break;
|
|
168
|
+
|
|
169
|
+
case MotionEvent.ACTION_MOVE:
|
|
170
|
+
// Only block scrolling if:
|
|
171
|
+
// - We're in a long press (selection menu is showing)
|
|
172
|
+
// - OR text is currently selected (user might be dragging handles)
|
|
173
|
+
if (isInLongPress || hasTextSelection()) {
|
|
174
|
+
if (getParent() != null) {
|
|
175
|
+
getParent().requestDisallowInterceptTouchEvent(true);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
|
|
180
|
+
case MotionEvent.ACTION_UP:
|
|
181
|
+
case MotionEvent.ACTION_CANCEL:
|
|
182
|
+
// Always release on touch end
|
|
183
|
+
isInLongPress = false;
|
|
184
|
+
if (getParent() != null) {
|
|
185
|
+
getParent().requestDisallowInterceptTouchEvent(false);
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return super.onTouchEvent(event);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Check if this TextView currently has selected text
|
|
196
|
+
*/
|
|
197
|
+
private boolean hasTextSelection() {
|
|
198
|
+
return getSelectionStart() != getSelectionEnd();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
@Override
|
|
202
|
+
public boolean performLongClick() {
|
|
203
|
+
// Mark that we're in a long press - this will prevent scrolling
|
|
204
|
+
// during the selection process
|
|
205
|
+
isInLongPress = true;
|
|
206
|
+
if (getParent() != null) {
|
|
207
|
+
getParent().requestDisallowInterceptTouchEvent(true);
|
|
208
|
+
}
|
|
209
|
+
return super.performLongClick();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
public void setMarkdownText(String text) {
|
|
213
|
+
if (text == null) text = "";
|
|
214
|
+
if (this.markdownText.equals(text)) return;
|
|
215
|
+
this.markdownText = text;
|
|
216
|
+
renderMarkdown();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private void renderMarkdown() {
|
|
220
|
+
if (this.markdownText == null) return;
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
markwon.setMarkdown(this, this.markdownText);
|
|
224
|
+
if (lineSpacing > 0) {
|
|
225
|
+
this.setLineSpacing(PixelUtil.toPixelFromDIP(lineSpacing), 1.0f);
|
|
226
|
+
}
|
|
227
|
+
} catch (Exception e) {
|
|
228
|
+
this.setText(this.markdownText);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (isTextSelectable()) {
|
|
232
|
+
// Re-apply properties ensuring it captures touches
|
|
233
|
+
this.setTextIsSelectable(true);
|
|
234
|
+
this.setFocusable(true);
|
|
235
|
+
this.setFocusableInTouchMode(true);
|
|
236
|
+
this.setClickable(true);
|
|
237
|
+
this.setLongClickable(true);
|
|
238
|
+
this.setMovementMethod(MarkdownMovementMethod.getInstance());
|
|
239
|
+
}
|
|
240
|
+
requestLayout();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private int markdownColor = Integer.MIN_VALUE;
|
|
244
|
+
|
|
245
|
+
public void setMarkdownColor(String colorStr) {
|
|
246
|
+
try {
|
|
247
|
+
int color = (colorStr != null && !colorStr.isEmpty()) ? Color.parseColor(colorStr) : Color.BLACK;
|
|
248
|
+
if (this.markdownColor == color) return;
|
|
249
|
+
this.markdownColor = color;
|
|
250
|
+
this.setTextColor(color);
|
|
251
|
+
rebuildMarkwon();
|
|
252
|
+
renderMarkdown();
|
|
253
|
+
} catch (Exception e) {
|
|
254
|
+
this.setTextColor(Color.BLACK);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private float markdownFontSize = Float.NaN;
|
|
259
|
+
|
|
260
|
+
public void setMarkdownFontSize(float size) {
|
|
261
|
+
if (size == this.markdownFontSize) return;
|
|
262
|
+
this.markdownFontSize = size;
|
|
263
|
+
this.setTextSize(size);
|
|
264
|
+
renderMarkdown();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
public void setMarkdownFontFamily(String fontFamily) {
|
|
268
|
+
if (fontFamily != null && !fontFamily.isEmpty()) {
|
|
269
|
+
try {
|
|
270
|
+
Typeface tf = ReactFontManager.getInstance().getTypeface(fontFamily, Typeface.NORMAL, this.context.getAssets());
|
|
271
|
+
if (tf == null) {
|
|
272
|
+
tf = Typeface.create(fontFamily, Typeface.NORMAL);
|
|
273
|
+
}
|
|
274
|
+
this.setTypeface(tf);
|
|
275
|
+
this.setTypeface(tf);
|
|
276
|
+
|
|
277
|
+
rebuildMarkwon();
|
|
278
|
+
renderMarkdown();
|
|
279
|
+
} catch (Exception e) {
|
|
280
|
+
// Keep default
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
public void setMarkdownSelectionColor(String color) {
|
|
286
|
+
try {
|
|
287
|
+
if (color != null && !color.isEmpty()) {
|
|
288
|
+
int parsedColor = Color.parseColor(color);
|
|
289
|
+
this.setHighlightColor(parsedColor);
|
|
290
|
+
|
|
291
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
292
|
+
Drawable left = getTextSelectHandleLeft();
|
|
293
|
+
if (left != null) {
|
|
294
|
+
left.mutate().setColorFilter(parsedColor, PorterDuff.Mode.SRC_IN);
|
|
295
|
+
setTextSelectHandleLeft(left);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
Drawable right = getTextSelectHandleRight();
|
|
299
|
+
if (right != null) {
|
|
300
|
+
right.mutate().setColorFilter(parsedColor, PorterDuff.Mode.SRC_IN);
|
|
301
|
+
setTextSelectHandleRight(right);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
Drawable handle = getTextSelectHandle();
|
|
305
|
+
if (handle != null) {
|
|
306
|
+
handle.mutate().setColorFilter(parsedColor, PorterDuff.Mode.SRC_IN);
|
|
307
|
+
setTextSelectHandle(handle);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} catch (Exception e) {
|
|
312
|
+
// Ignore
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
public void setMarkdownSelectable(boolean selectable) {
|
|
317
|
+
this.setTextIsSelectable(selectable);
|
|
318
|
+
this.setFocusable(selectable);
|
|
319
|
+
this.setFocusableInTouchMode(selectable);
|
|
320
|
+
|
|
321
|
+
// We do NOT set the movement method here directly anymore if we rely on renderMarkdown
|
|
322
|
+
// determining it, OR we set it here but ensure renderMarkdown respects it.
|
|
323
|
+
// Actually, renderMarkdown re-applies it if isTextSelectable() is true.
|
|
324
|
+
// So just calling requestLayout() or letting the next render pass handle it?
|
|
325
|
+
// Better:
|
|
326
|
+
if (selectable) {
|
|
327
|
+
this.setMovementMethod(MarkdownMovementMethod.getInstance());
|
|
328
|
+
} else {
|
|
329
|
+
this.setMovementMethod(LinkMovementMethod.getInstance());
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
public void setMarkdownScrollEnabled(boolean scrollEnabled) {
|
|
334
|
+
this.setVerticalScrollBarEnabled(scrollEnabled);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// New customization methods
|
|
338
|
+
public void setMarkdownLinkColor(String color) {
|
|
339
|
+
try {
|
|
340
|
+
if (color != null && !color.isEmpty()) {
|
|
341
|
+
this.linkColor = Color.parseColor(color);
|
|
342
|
+
rebuildMarkwon();
|
|
343
|
+
renderMarkdown();
|
|
344
|
+
}
|
|
345
|
+
} catch (Exception e) {
|
|
346
|
+
// Ignore
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
public void setMarkdownLineSpacing(float spacing) {
|
|
351
|
+
this.lineSpacing = spacing;
|
|
352
|
+
if (spacing > 0) {
|
|
353
|
+
this.setLineSpacing(PixelUtil.toPixelFromDIP(spacing), 1.0f);
|
|
354
|
+
} else {
|
|
355
|
+
this.setLineSpacing(0, 1.0f);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
public void setMarkdownParagraphSpacing(float spacing) {
|
|
360
|
+
this.paragraphSpacing = spacing;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
public void setMarkdownBulletIndent(float indent) {
|
|
364
|
+
this.bulletIndent = indent;
|
|
365
|
+
rebuildMarkwon();
|
|
366
|
+
renderMarkdown();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
public void setMarkdownListLeftInset(float inset) {
|
|
370
|
+
this.listLeftInset = inset;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
public void setMarkdownListSpacingBefore(float spacing) {
|
|
374
|
+
this.listSpacingBefore = spacing;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
public void setMarkdownListSpacingAfter(float spacing) {
|
|
378
|
+
this.listSpacingAfter = spacing;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private static class MarkdownMovementMethod extends ArrowKeyMovementMethod {
|
|
382
|
+
private static MarkdownMovementMethod instance;
|
|
383
|
+
|
|
384
|
+
public static android.text.method.MovementMethod getInstance() {
|
|
385
|
+
if (instance == null) {
|
|
386
|
+
instance = new MarkdownMovementMethod();
|
|
387
|
+
}
|
|
388
|
+
return instance;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
@Override
|
|
392
|
+
public boolean onTouchEvent(android.widget.TextView widget, android.text.Spannable buffer, android.view.MotionEvent event) {
|
|
393
|
+
int action = event.getAction();
|
|
394
|
+
|
|
395
|
+
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
|
|
396
|
+
int x = (int) event.getX();
|
|
397
|
+
int y = (int) event.getY();
|
|
398
|
+
|
|
399
|
+
x -= widget.getTotalPaddingLeft();
|
|
400
|
+
y -= widget.getTotalPaddingTop();
|
|
401
|
+
|
|
402
|
+
x += widget.getScrollX();
|
|
403
|
+
y += widget.getScrollY();
|
|
404
|
+
|
|
405
|
+
Layout layout = widget.getLayout();
|
|
406
|
+
// Check if layout is null
|
|
407
|
+
if (layout == null) {
|
|
408
|
+
return super.onTouchEvent(widget, buffer, event);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
int line = layout.getLineForVertical(y);
|
|
412
|
+
|
|
413
|
+
if (x < layout.getLineLeft(line) || x > layout.getLineRight(line)) {
|
|
414
|
+
return super.onTouchEvent(widget, buffer, event);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
int off = layout.getOffsetForHorizontal(line, x);
|
|
418
|
+
|
|
419
|
+
ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class);
|
|
420
|
+
|
|
421
|
+
if (links.length != 0) {
|
|
422
|
+
ClickableSpan link = links[0];
|
|
423
|
+
|
|
424
|
+
int start = buffer.getSpanStart(link);
|
|
425
|
+
int end = buffer.getSpanEnd(link);
|
|
426
|
+
|
|
427
|
+
if (off >= start && off < end) {
|
|
428
|
+
if (action == MotionEvent.ACTION_UP) {
|
|
429
|
+
if (android.text.Selection.getSelectionStart(buffer) == android.text.Selection.getSelectionEnd(buffer)) {
|
|
430
|
+
link.onClick(widget);
|
|
431
|
+
}
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
else if (action == MotionEvent.ACTION_DOWN) {
|
|
435
|
+
// We don't return true here for links, because we want to allow long press
|
|
436
|
+
// for selection to still work even if the touch started on a link.
|
|
437
|
+
// super.onTouchEvent() handles the long press check.
|
|
438
|
+
return super.onTouchEvent(widget, buffer, event);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return super.onTouchEvent(widget, buffer, event);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// MARK: - Rules Parsing and Application
|
|
448
|
+
|
|
449
|
+
public void setMarkdownRulesJson(String rulesJson) {
|
|
450
|
+
if (rulesJson == null) rulesJson = "";
|
|
451
|
+
if (this.markdownRulesJson.equals(rulesJson)) return;
|
|
452
|
+
this.markdownRulesJson = rulesJson;
|
|
453
|
+
parseAndApplyRules();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private void parseAndApplyRules() {
|
|
457
|
+
if (markdownRulesJson == null || markdownRulesJson.isEmpty()) {
|
|
458
|
+
parsedRules = null;
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
parsedRules = new org.json.JSONObject(markdownRulesJson);
|
|
464
|
+
rebuildMarkwon();
|
|
465
|
+
renderMarkdown();
|
|
466
|
+
} catch (org.json.JSONException e) {
|
|
467
|
+
parsedRules = null;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private int getRuleColorValue(String elementType, String key, int defaultValue) {
|
|
472
|
+
if (parsedRules == null) return defaultValue;
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
org.json.JSONObject element = parsedRules.optJSONObject(elementType);
|
|
476
|
+
if (element == null) return defaultValue;
|
|
477
|
+
|
|
478
|
+
String colorStr = element.optString(key, null);
|
|
479
|
+
if (colorStr != null && !colorStr.isEmpty()) {
|
|
480
|
+
return Color.parseColor(colorStr);
|
|
481
|
+
}
|
|
482
|
+
} catch (Exception e) {
|
|
483
|
+
// Ignore parsing errors
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return defaultValue;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private float getRuleFloatValue(String elementType, String key, float defaultValue) {
|
|
490
|
+
if (parsedRules == null) return defaultValue;
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
org.json.JSONObject element = parsedRules.optJSONObject(elementType);
|
|
494
|
+
if (element == null) return defaultValue;
|
|
495
|
+
|
|
496
|
+
return (float) element.optDouble(key, defaultValue);
|
|
497
|
+
} catch (Exception e) {
|
|
498
|
+
return defaultValue;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private int getRuleIntValue(String elementType, String key, int defaultValue) {
|
|
503
|
+
if (parsedRules == null) return defaultValue;
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
org.json.JSONObject element = parsedRules.optJSONObject(elementType);
|
|
507
|
+
if (element == null) return defaultValue;
|
|
508
|
+
|
|
509
|
+
return element.optInt(key, defaultValue);
|
|
510
|
+
} catch (Exception e) {
|
|
511
|
+
return defaultValue;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
package com.markdownnative;
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.common.MapBuilder;
|
|
4
|
+
import com.facebook.react.uimanager.SimpleViewManager;
|
|
5
|
+
import com.facebook.react.uimanager.ThemedReactContext;
|
|
6
|
+
import com.facebook.react.uimanager.annotations.ReactProp;
|
|
7
|
+
import java.util.Map;
|
|
8
|
+
|
|
9
|
+
public class MarkdownViewManager extends SimpleViewManager<MarkdownView> {
|
|
10
|
+
|
|
11
|
+
public static final String REACT_CLASS = "MarkdownView";
|
|
12
|
+
|
|
13
|
+
@Override
|
|
14
|
+
public String getName() {
|
|
15
|
+
return REACT_CLASS;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Override
|
|
19
|
+
public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
|
|
20
|
+
return MapBuilder.<String, Object>builder()
|
|
21
|
+
.put("onNativeSizeChange", MapBuilder.of("registrationName", "onNativeSizeChange"))
|
|
22
|
+
.build();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Override
|
|
26
|
+
protected MarkdownView createViewInstance(ThemedReactContext reactContext) {
|
|
27
|
+
return new MarkdownView(reactContext);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@ReactProp(name = "markdownText")
|
|
31
|
+
public void setMarkdownText(MarkdownView view, String text) {
|
|
32
|
+
view.setMarkdownText(text);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@ReactProp(name = "markdownColor")
|
|
36
|
+
public void setMarkdownColor(MarkdownView view, String color) {
|
|
37
|
+
view.setMarkdownColor(color);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@ReactProp(name = "markdownFontSize")
|
|
41
|
+
public void setMarkdownFontSize(MarkdownView view, float size) {
|
|
42
|
+
view.setMarkdownFontSize(size);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@ReactProp(name = "markdownFontFamily")
|
|
46
|
+
public void setMarkdownFontFamily(MarkdownView view, String fontFamily) {
|
|
47
|
+
view.setMarkdownFontFamily(fontFamily);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@ReactProp(name = "markdownSelectionColor")
|
|
51
|
+
public void setMarkdownSelectionColor(MarkdownView view, String color) {
|
|
52
|
+
view.setMarkdownSelectionColor(color);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@ReactProp(name = "markdownSelectable")
|
|
56
|
+
public void setMarkdownSelectable(MarkdownView view, boolean selectable) {
|
|
57
|
+
view.setMarkdownSelectable(selectable);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@ReactProp(name = "markdownScrollEnabled")
|
|
61
|
+
public void setMarkdownScrollEnabled(MarkdownView view, boolean scrollEnabled) {
|
|
62
|
+
view.setMarkdownScrollEnabled(scrollEnabled);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// New customization props
|
|
66
|
+
@ReactProp(name = "markdownLinkColor")
|
|
67
|
+
public void setMarkdownLinkColor(MarkdownView view, String color) {
|
|
68
|
+
view.setMarkdownLinkColor(color);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@ReactProp(name = "markdownLineSpacing", defaultFloat = 0f)
|
|
72
|
+
public void setMarkdownLineSpacing(MarkdownView view, float spacing) {
|
|
73
|
+
view.setMarkdownLineSpacing(spacing);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@ReactProp(name = "markdownParagraphSpacing", defaultFloat = 0f)
|
|
77
|
+
public void setMarkdownParagraphSpacing(MarkdownView view, float spacing) {
|
|
78
|
+
view.setMarkdownParagraphSpacing(spacing);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@ReactProp(name = "markdownBulletIndent", defaultFloat = 0f)
|
|
82
|
+
public void setMarkdownBulletIndent(MarkdownView view, float indent) {
|
|
83
|
+
view.setMarkdownBulletIndent(indent);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@ReactProp(name = "markdownListLeftInset", defaultFloat = 0f)
|
|
87
|
+
public void setMarkdownListLeftInset(MarkdownView view, float inset) {
|
|
88
|
+
view.setMarkdownListLeftInset(inset);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@ReactProp(name = "markdownListSpacingBefore", defaultFloat = 0f)
|
|
92
|
+
public void setMarkdownListSpacingBefore(MarkdownView view, float spacing) {
|
|
93
|
+
view.setMarkdownListSpacingBefore(spacing);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@ReactProp(name = "markdownListSpacingAfter", defaultFloat = 0f)
|
|
97
|
+
public void setMarkdownListSpacingAfter(MarkdownView view, float spacing) {
|
|
98
|
+
view.setMarkdownListSpacingAfter(spacing);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// New rules-based styling system
|
|
102
|
+
@ReactProp(name = "markdownRulesJson")
|
|
103
|
+
public void setMarkdownRulesJson(MarkdownView view, String rulesJson) {
|
|
104
|
+
view.setMarkdownRulesJson(rulesJson);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|