react-native-jet-markdown 0.2.0-beta.7
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/JetMarkdown.podspec +42 -0
- package/LICENSE +20 -0
- package/README.md +247 -0
- package/android/CMakeLists.txt +52 -0
- package/android/build.gradle +55 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/JniBindings.cpp +243 -0
- package/android/src/main/java/com/jetmarkdown/JetMarkdownNative.kt +114 -0
- package/android/src/main/java/com/jetmarkdown/JetMarkdownPackage.kt +22 -0
- package/android/src/main/java/com/jetmarkdown/JetMarkdownView.kt +235 -0
- package/android/src/main/java/com/jetmarkdown/JetMarkdownViewManager.kt +75 -0
- package/android/src/main/java/com/jetmarkdown/editor/EditorSpans.kt +217 -0
- package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorManager.kt +224 -0
- package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorView.kt +1736 -0
- package/android/src/main/java/com/jetmarkdown/measure/MarkdownMeasurer.kt +54 -0
- package/android/src/main/java/com/jetmarkdown/parser/AstDecoder.kt +59 -0
- package/android/src/main/java/com/jetmarkdown/parser/MdNode.kt +43 -0
- package/android/src/main/java/com/jetmarkdown/render/ContentCache.kt +53 -0
- package/android/src/main/java/com/jetmarkdown/render/RenderedContent.kt +318 -0
- package/android/src/main/java/com/jetmarkdown/render/SpannableRenderer.kt +658 -0
- package/android/src/main/java/com/jetmarkdown/render/spans/ChipSpan.kt +17 -0
- package/android/src/main/java/com/jetmarkdown/render/spans/InteractiveSpans.kt +11 -0
- package/android/src/main/java/com/jetmarkdown/render/spans/MarkdownLineHeightSpan.kt +59 -0
- package/android/src/main/java/com/jetmarkdown/render/spans/RunSpan.kt +40 -0
- package/android/src/main/java/com/jetmarkdown/style/Fonts.kt +34 -0
- package/android/src/main/java/com/jetmarkdown/style/LayoutStyleSpec.kt +71 -0
- package/android/src/main/java/com/jetmarkdown/style/PlatformColorResolver.kt +77 -0
- package/android/src/main/java/com/jetmarkdown/style/StyleConfig.kt +80 -0
- package/android/src/main/java/com/jetmarkdown/style/TextStyleSpec.kt +61 -0
- package/android/src/main/java/com/jetmarkdown/views/BlockStackView.kt +292 -0
- package/android/src/main/java/com/jetmarkdown/views/BlockTextView.kt +378 -0
- package/android/src/main/java/com/jetmarkdown/views/BoxDrawing.kt +50 -0
- package/android/src/main/java/com/jetmarkdown/views/MarkdownHost.kt +12 -0
- package/android/src/main/java/com/jetmarkdown/views/MarkdownImageView.kt +113 -0
- package/android/src/main/java/com/jetmarkdown/views/NestedHorizontalScrollView.kt +87 -0
- package/android/src/main/java/com/jetmarkdown/views/TableBlockView.kt +139 -0
- package/cpp/core/Ast.h +85 -0
- package/cpp/core/AstJson.cpp +129 -0
- package/cpp/core/AstJson.h +12 -0
- package/cpp/core/AstSerializer.cpp +40 -0
- package/cpp/core/AstSerializer.h +21 -0
- package/cpp/core/AstToMarkdown.cpp +549 -0
- package/cpp/core/AstToMarkdown.h +16 -0
- package/cpp/core/EditorRuns.cpp +640 -0
- package/cpp/core/EditorRuns.h +94 -0
- package/cpp/core/EditorText.cpp +15 -0
- package/cpp/core/EditorText.h +18 -0
- package/cpp/core/InlineExtensions.cpp +305 -0
- package/cpp/core/InlineExtensions.h +17 -0
- package/cpp/core/Parser.cpp +470 -0
- package/cpp/core/Parser.h +16 -0
- package/cpp/core/Preprocess.cpp +181 -0
- package/cpp/core/Preprocess.h +14 -0
- package/cpp/md4c/LICENSE +22 -0
- package/cpp/md4c/VERSION +1 -0
- package/cpp/md4c/md4c.c +6462 -0
- package/cpp/md4c/md4c.h +407 -0
- package/cpp/react/JetMarkdownEditorShadowNode.cpp +54 -0
- package/cpp/react/JetMarkdownEditorShadowNode.h +55 -0
- package/cpp/react/JetMarkdownEditorState.h +35 -0
- package/cpp/react/JetMarkdownMeasurer.cpp +32 -0
- package/cpp/react/JetMarkdownMeasurer.h +41 -0
- package/cpp/react/JetMarkdownShadowNode.cpp +100 -0
- package/cpp/react/JetMarkdownShadowNode.h +53 -0
- package/cpp/react/JetMarkdownState.h +52 -0
- package/cpp/react/override/react/renderer/components/JetMarkdownViewSpec/ComponentDescriptors.h +21 -0
- package/cpp/tests/parser_tests.cpp +662 -0
- package/cpp/tests/run_tests.sh +25 -0
- package/ios/JetMarkdownView.h +14 -0
- package/ios/JetMarkdownView.mm +468 -0
- package/ios/editor/JetMarkdownEditor.h +12 -0
- package/ios/editor/JetMarkdownEditor.mm +2002 -0
- package/ios/measure/JMDMarkdownMeasurer.h +22 -0
- package/ios/measure/JMDMarkdownMeasurer.mm +38 -0
- package/ios/render/JMDBlock.h +97 -0
- package/ios/render/JMDBlock.m +28 -0
- package/ios/render/JMDBlockRenderer.h +17 -0
- package/ios/render/JMDBlockRenderer.mm +852 -0
- package/ios/render/JMDContentCache.h +17 -0
- package/ios/render/JMDContentCache.mm +42 -0
- package/ios/render/JMDRenderedContent.h +35 -0
- package/ios/render/JMDRenderedContent.mm +274 -0
- package/ios/style/JMDFontScale.h +33 -0
- package/ios/style/JMDLayoutStyle.h +37 -0
- package/ios/style/JMDLayoutStyle.mm +68 -0
- package/ios/style/JMDStyleConfig.h +39 -0
- package/ios/style/JMDStyleConfig.mm +111 -0
- package/ios/style/JMDTextStyle.h +25 -0
- package/ios/style/JMDTextStyle.mm +68 -0
- package/ios/views/JMDBlockStackView.h +18 -0
- package/ios/views/JMDBlockStackView.m +97 -0
- package/ios/views/JMDBlockTextView.h +24 -0
- package/ios/views/JMDBlockTextView.m +340 -0
- package/ios/views/JMDBoxViews.h +33 -0
- package/ios/views/JMDBoxViews.m +292 -0
- package/ios/views/JMDImageView.h +19 -0
- package/ios/views/JMDImageView.m +68 -0
- package/ios/views/JMDMarkdownHost.h +16 -0
- package/ios/views/JMDTableView.h +16 -0
- package/ios/views/JMDTableView.m +172 -0
- package/lib/module/JetMarkdownEditor.js +85 -0
- package/lib/module/JetMarkdownEditor.js.map +1 -0
- package/lib/module/JetMarkdownEditor.native.js +244 -0
- package/lib/module/JetMarkdownEditor.native.js.map +1 -0
- package/lib/module/JetMarkdownEditorNativeComponent.ts +173 -0
- package/lib/module/JetMarkdownView.js +19 -0
- package/lib/module/JetMarkdownView.js.map +1 -0
- package/lib/module/JetMarkdownView.native.js +44 -0
- package/lib/module/JetMarkdownView.native.js.map +1 -0
- package/lib/module/JetMarkdownViewNativeComponent.ts +35 -0
- package/lib/module/defaultStyles.js +122 -0
- package/lib/module/defaultStyles.js.map +1 -0
- package/lib/module/index.js +9 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/serializeStyles.js +259 -0
- package/lib/module/serializeStyles.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useJetMarkdownEditor.js +41 -0
- package/lib/module/useJetMarkdownEditor.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/JetMarkdownEditor.d.ts +6 -0
- package/lib/typescript/src/JetMarkdownEditor.d.ts.map +1 -0
- package/lib/typescript/src/JetMarkdownEditor.native.d.ts +6 -0
- package/lib/typescript/src/JetMarkdownEditor.native.d.ts.map +1 -0
- package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts +105 -0
- package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/JetMarkdownView.d.ts +3 -0
- package/lib/typescript/src/JetMarkdownView.d.ts.map +1 -0
- package/lib/typescript/src/JetMarkdownView.native.d.ts +3 -0
- package/lib/typescript/src/JetMarkdownView.native.d.ts.map +1 -0
- package/lib/typescript/src/JetMarkdownViewNativeComponent.d.ts +28 -0
- package/lib/typescript/src/JetMarkdownViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/defaultStyles.d.ts +20 -0
- package/lib/typescript/src/defaultStyles.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +7 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/serializeStyles.d.ts +34 -0
- package/lib/typescript/src/serializeStyles.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +352 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/useJetMarkdownEditor.d.ts +36 -0
- package/lib/typescript/src/useJetMarkdownEditor.d.ts.map +1 -0
- package/package.json +150 -0
- package/react-native.config.js +11 -0
- package/src/JetMarkdownEditor.native.tsx +301 -0
- package/src/JetMarkdownEditor.tsx +90 -0
- package/src/JetMarkdownEditorNativeComponent.ts +173 -0
- package/src/JetMarkdownView.native.tsx +67 -0
- package/src/JetMarkdownView.tsx +16 -0
- package/src/JetMarkdownViewNativeComponent.ts +35 -0
- package/src/defaultStyles.ts +102 -0
- package/src/index.tsx +34 -0
- package/src/serializeStyles.ts +369 -0
- package/src/types.ts +438 -0
- package/src/useJetMarkdownEditor.ts +50 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
package com.jetmarkdown.views
|
|
2
|
+
|
|
3
|
+
import android.graphics.Canvas
|
|
4
|
+
import android.graphics.Paint
|
|
5
|
+
import android.graphics.RectF
|
|
6
|
+
import com.jetmarkdown.style.LayoutStyleSpec
|
|
7
|
+
|
|
8
|
+
/** Shared background + per-side border painting for block containers. */
|
|
9
|
+
object BoxDrawing {
|
|
10
|
+
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
11
|
+
|
|
12
|
+
fun draw(canvas: Canvas, style: LayoutStyleSpec, width: Float, height: Float) {
|
|
13
|
+
if (style.backgroundColor != null) {
|
|
14
|
+
paint.style = Paint.Style.FILL
|
|
15
|
+
paint.color = style.backgroundColor
|
|
16
|
+
canvas.drawRoundRect(
|
|
17
|
+
RectF(0f, 0f, width, height),
|
|
18
|
+
style.borderRadius,
|
|
19
|
+
style.borderRadius,
|
|
20
|
+
paint,
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
paint.style = Paint.Style.FILL
|
|
24
|
+
if (style.borderLeftWidth > 0 && style.borderLeftColor != null) {
|
|
25
|
+
paint.color = style.borderLeftColor
|
|
26
|
+
if (style.borderRadius > 0) {
|
|
27
|
+
canvas.drawRoundRect(
|
|
28
|
+
RectF(0f, 0f, style.borderLeftWidth, height),
|
|
29
|
+
style.borderRadius / 2,
|
|
30
|
+
style.borderRadius / 2,
|
|
31
|
+
paint,
|
|
32
|
+
)
|
|
33
|
+
} else {
|
|
34
|
+
canvas.drawRect(0f, 0f, style.borderLeftWidth, height, paint)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (style.borderRightWidth > 0 && style.borderRightColor != null) {
|
|
38
|
+
paint.color = style.borderRightColor
|
|
39
|
+
canvas.drawRect(width - style.borderRightWidth, 0f, width, height, paint)
|
|
40
|
+
}
|
|
41
|
+
if (style.borderTopWidth > 0 && style.borderTopColor != null) {
|
|
42
|
+
paint.color = style.borderTopColor
|
|
43
|
+
canvas.drawRect(0f, 0f, width, style.borderTopWidth, paint)
|
|
44
|
+
}
|
|
45
|
+
if (style.borderBottomWidth > 0 && style.borderBottomColor != null) {
|
|
46
|
+
paint.color = style.borderBottomColor
|
|
47
|
+
canvas.drawRect(0f, height - style.borderBottomWidth, width, height, paint)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
package com.jetmarkdown.views
|
|
2
|
+
|
|
3
|
+
/** Callbacks from block views up to the host component view. */
|
|
4
|
+
interface MarkdownHost {
|
|
5
|
+
fun onImageIntrinsicSize(url: String, widthDp: Float, heightDp: Float)
|
|
6
|
+
fun isSpoilerRevealed(id: Int): Boolean
|
|
7
|
+
fun toggleSpoiler(id: Int)
|
|
8
|
+
fun onLinkPress(url: String)
|
|
9
|
+
fun onLinkLongPress(url: String)
|
|
10
|
+
/** Position and size describe the pressed image in screen coordinates, dp. */
|
|
11
|
+
fun onImagePress(url: String, xDp: Float, yDp: Float, widthDp: Float, heightDp: Float)
|
|
12
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
package com.jetmarkdown.views
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.graphics.Canvas
|
|
6
|
+
import android.graphics.Path
|
|
7
|
+
import android.graphics.RectF
|
|
8
|
+
import android.graphics.drawable.Drawable
|
|
9
|
+
import android.widget.ImageView
|
|
10
|
+
import com.bumptech.glide.Glide
|
|
11
|
+
import com.bumptech.glide.load.DataSource
|
|
12
|
+
import com.bumptech.glide.load.engine.GlideException
|
|
13
|
+
import com.bumptech.glide.request.RequestListener
|
|
14
|
+
import com.bumptech.glide.request.target.Target
|
|
15
|
+
import com.jetmarkdown.render.Block
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* One markdown image, loaded through Glide: animated GIF playback,
|
|
19
|
+
* downsampling, memory + disk caches, in-flight request sharing, and
|
|
20
|
+
* cancellation when the view is rebound. Background shows while loading
|
|
21
|
+
* (and stays for broken URLs); rounded corners clip both.
|
|
22
|
+
*/
|
|
23
|
+
@SuppressLint("AppCompatCustomView")
|
|
24
|
+
class MarkdownImageView(context: Context) : ImageView(context) {
|
|
25
|
+
private var block: Block.Image? = null
|
|
26
|
+
private val clipPath = Path()
|
|
27
|
+
|
|
28
|
+
var host: MarkdownHost? = null
|
|
29
|
+
|
|
30
|
+
init {
|
|
31
|
+
scaleType = ScaleType.FIT_CENTER
|
|
32
|
+
setOnClickListener {
|
|
33
|
+
block?.let { image ->
|
|
34
|
+
val density = resources.displayMetrics.density
|
|
35
|
+
val location = IntArray(2)
|
|
36
|
+
getLocationOnScreen(location)
|
|
37
|
+
host?.onImagePress(
|
|
38
|
+
image.url,
|
|
39
|
+
location[0] / density,
|
|
40
|
+
location[1] / density,
|
|
41
|
+
width / density,
|
|
42
|
+
height / density,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fun bind(image: Block.Image) {
|
|
49
|
+
block = image
|
|
50
|
+
// Application context: requests outlive transient view detaches during
|
|
51
|
+
// list recycling and are replaced (cancelling the old one) on rebind.
|
|
52
|
+
Glide.with(context.applicationContext)
|
|
53
|
+
.load(image.url.ifEmpty { null })
|
|
54
|
+
.listener(object : RequestListener<Drawable> {
|
|
55
|
+
override fun onLoadFailed(
|
|
56
|
+
e: GlideException?,
|
|
57
|
+
model: Any?,
|
|
58
|
+
target: Target<Drawable>,
|
|
59
|
+
isFirstResource: Boolean,
|
|
60
|
+
): Boolean = false
|
|
61
|
+
|
|
62
|
+
override fun onResourceReady(
|
|
63
|
+
resource: Drawable,
|
|
64
|
+
model: Any,
|
|
65
|
+
target: Target<Drawable>?,
|
|
66
|
+
dataSource: DataSource,
|
|
67
|
+
isFirstResource: Boolean,
|
|
68
|
+
): Boolean {
|
|
69
|
+
if (block?.url == image.url) {
|
|
70
|
+
// Image pixels map 1:1 to dp (web semantics); reported for
|
|
71
|
+
// cache hits too so a fresh view still resizes un-presized
|
|
72
|
+
// images.
|
|
73
|
+
host?.onImageIntrinsicSize(
|
|
74
|
+
image.url,
|
|
75
|
+
resource.intrinsicWidth.toFloat(),
|
|
76
|
+
resource.intrinsicHeight.toFloat(),
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
.into(this)
|
|
83
|
+
invalidate()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Discard hook (views are recreated wholesale on rebind): stops the
|
|
88
|
+
* in-flight request from delivering into a recycled host and frees GIF
|
|
89
|
+
* buffers early. Transient detaches during scrolling must NOT clear —
|
|
90
|
+
* this is only called when the parent drops the view for good.
|
|
91
|
+
*/
|
|
92
|
+
fun unbind() {
|
|
93
|
+
host = null
|
|
94
|
+
block = null
|
|
95
|
+
Glide.with(context.applicationContext).clear(this)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
override fun onDraw(canvas: Canvas) {
|
|
99
|
+
val image = block ?: return
|
|
100
|
+
if (image.borderRadiusPx > 0) {
|
|
101
|
+
clipPath.reset()
|
|
102
|
+
clipPath.addRoundRect(
|
|
103
|
+
RectF(0f, 0f, width.toFloat(), height.toFloat()),
|
|
104
|
+
image.borderRadiusPx,
|
|
105
|
+
image.borderRadiusPx,
|
|
106
|
+
Path.Direction.CW,
|
|
107
|
+
)
|
|
108
|
+
canvas.clipPath(clipPath)
|
|
109
|
+
}
|
|
110
|
+
image.backgroundColor?.let { canvas.drawColor(it) }
|
|
111
|
+
super.onDraw(canvas)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
package com.jetmarkdown.views
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.view.MotionEvent
|
|
5
|
+
import android.view.ViewConfiguration
|
|
6
|
+
import android.widget.HorizontalScrollView
|
|
7
|
+
import com.facebook.react.uimanager.events.NativeGestureUtil
|
|
8
|
+
import kotlin.math.abs
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Horizontal scroller for code blocks and tables.
|
|
12
|
+
*
|
|
13
|
+
* Two problems to solve, both because this is not a React view:
|
|
14
|
+
* 1. Once a wrapping RN Pressable becomes the JS responder, Fabric's
|
|
15
|
+
* JSResponderHandler makes that view intercept every non-UP event, which
|
|
16
|
+
* cancels this scroller's stream (~40ms after DOWN). Guard against it by
|
|
17
|
+
* disallowing ancestor interception while a touch may become a horizontal
|
|
18
|
+
* drag, then hand the gesture back the moment it proves vertical so the
|
|
19
|
+
* outer list still pans.
|
|
20
|
+
* 2. Nothing cancels the JS responder (the Pressable press) when this view
|
|
21
|
+
* takes the drag — notify the React root like ReactHorizontalScrollView
|
|
22
|
+
* does, so onPress does not fire after a scroll.
|
|
23
|
+
*/
|
|
24
|
+
class NestedHorizontalScrollView(context: Context) : HorizontalScrollView(context) {
|
|
25
|
+
private var downX = 0f
|
|
26
|
+
private var downY = 0f
|
|
27
|
+
private var notified = false
|
|
28
|
+
private var directionLocked = false
|
|
29
|
+
|
|
30
|
+
private fun hasOverflow(): Boolean =
|
|
31
|
+
canScrollHorizontally(1) || canScrollHorizontally(-1)
|
|
32
|
+
|
|
33
|
+
private fun onDown(ev: MotionEvent) {
|
|
34
|
+
downX = ev.x
|
|
35
|
+
downY = ev.y
|
|
36
|
+
notified = false
|
|
37
|
+
directionLocked = false
|
|
38
|
+
if (hasOverflow()) {
|
|
39
|
+
parent?.requestDisallowInterceptTouchEvent(true)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private fun onMove(ev: MotionEvent) {
|
|
44
|
+
if (directionLocked) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
val dx = abs(ev.x - downX)
|
|
48
|
+
val dy = abs(ev.y - downY)
|
|
49
|
+
val slop = ViewConfiguration.get(context).scaledTouchSlop
|
|
50
|
+
if (dx > slop || dy > slop) {
|
|
51
|
+
directionLocked = true
|
|
52
|
+
if (dy > dx) {
|
|
53
|
+
// Vertical drag: let the outer list intercept and pan.
|
|
54
|
+
parent?.requestDisallowInterceptTouchEvent(false)
|
|
55
|
+
} else {
|
|
56
|
+
notifyGestureStarted(ev)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
|
|
62
|
+
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
|
|
63
|
+
onDown(ev)
|
|
64
|
+
}
|
|
65
|
+
if (super.onInterceptTouchEvent(ev)) {
|
|
66
|
+
notifyGestureStarted(ev)
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override fun onTouchEvent(ev: MotionEvent): Boolean {
|
|
73
|
+
when (ev.actionMasked) {
|
|
74
|
+
MotionEvent.ACTION_MOVE -> onMove(ev)
|
|
75
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL ->
|
|
76
|
+
parent?.requestDisallowInterceptTouchEvent(false)
|
|
77
|
+
}
|
|
78
|
+
return super.onTouchEvent(ev)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private fun notifyGestureStarted(ev: MotionEvent) {
|
|
82
|
+
if (!notified) {
|
|
83
|
+
notified = true
|
|
84
|
+
NativeGestureUtil.notifyNativeGestureStarted(this, ev)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
package com.jetmarkdown.views
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Canvas
|
|
5
|
+
import android.view.ViewGroup
|
|
6
|
+
import com.jetmarkdown.render.Block
|
|
7
|
+
import com.jetmarkdown.render.MeasuredBlock
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Table: paints the table box, hosts a grid in a horizontal scroller so
|
|
11
|
+
* wide tables keep readable column widths.
|
|
12
|
+
*/
|
|
13
|
+
class TableBlockView(context: Context) : ViewGroup(context) {
|
|
14
|
+
private var measured: MeasuredBlock? = null
|
|
15
|
+
private var block: Block.Table? = null
|
|
16
|
+
private val scroller = NestedHorizontalScrollView(context).apply {
|
|
17
|
+
isHorizontalScrollBarEnabled = false
|
|
18
|
+
overScrollMode = OVER_SCROLL_NEVER
|
|
19
|
+
}
|
|
20
|
+
private val grid = TableGridView(context)
|
|
21
|
+
|
|
22
|
+
init {
|
|
23
|
+
setWillNotDraw(false)
|
|
24
|
+
scroller.addView(grid)
|
|
25
|
+
addView(scroller)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fun bind(measuredBlock: MeasuredBlock, table: Block.Table, host: MarkdownHost? = null) {
|
|
29
|
+
measured = measuredBlock
|
|
30
|
+
block = table
|
|
31
|
+
grid.bind(measuredBlock, table, host)
|
|
32
|
+
invalidate()
|
|
33
|
+
requestLayout()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override fun onDraw(canvas: Canvas) {
|
|
37
|
+
block?.let { BoxDrawing.draw(canvas, it.style, width.toFloat(), height.toFloat()) }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
41
|
+
setMeasuredDimension(
|
|
42
|
+
MeasureSpec.getSize(widthMeasureSpec),
|
|
43
|
+
MeasureSpec.getSize(heightMeasureSpec),
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
|
|
48
|
+
val style = block?.style ?: return
|
|
49
|
+
val tree = measured ?: return
|
|
50
|
+
val left = (style.borderLeftWidth + style.paddingLeft).toInt()
|
|
51
|
+
val top = (style.borderTopWidth + style.paddingTop).toInt()
|
|
52
|
+
val right = (r - l) - (style.borderRightWidth + style.paddingRight).toInt()
|
|
53
|
+
val gridHeight = tree.rowHeights.sum().toInt()
|
|
54
|
+
|
|
55
|
+
scroller.measure(
|
|
56
|
+
MeasureSpec.makeMeasureSpec(right - left, MeasureSpec.EXACTLY),
|
|
57
|
+
MeasureSpec.makeMeasureSpec(gridHeight, MeasureSpec.EXACTLY),
|
|
58
|
+
)
|
|
59
|
+
scroller.layout(left, top, right, top + gridHeight)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override fun shouldDelayChildPressedState(): Boolean = false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The unclipped grid inside the scroller: row boxes + cell text. */
|
|
66
|
+
class TableGridView(context: Context) : ViewGroup(context) {
|
|
67
|
+
private var measured: MeasuredBlock? = null
|
|
68
|
+
private var block: Block.Table? = null
|
|
69
|
+
|
|
70
|
+
init {
|
|
71
|
+
setWillNotDraw(false)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fun bind(measuredBlock: MeasuredBlock, table: Block.Table, host: MarkdownHost? = null) {
|
|
75
|
+
measured = measuredBlock
|
|
76
|
+
block = table
|
|
77
|
+
removeAllViews()
|
|
78
|
+
for (row in measuredBlock.cellLayouts) {
|
|
79
|
+
for (cell in row) {
|
|
80
|
+
addView(BlockTextView(context).apply {
|
|
81
|
+
this.host = host
|
|
82
|
+
setTextLayout(cell)
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
invalidate()
|
|
87
|
+
requestLayout()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
91
|
+
val tree = measured
|
|
92
|
+
setMeasuredDimension(
|
|
93
|
+
tree?.contentWidthPx?.toInt() ?: 0,
|
|
94
|
+
tree?.rowHeights?.sum()?.toInt() ?: 0,
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
override fun onDraw(canvas: Canvas) {
|
|
99
|
+
val table = block ?: return
|
|
100
|
+
val tree = measured ?: return
|
|
101
|
+
val width = tree.contentWidthPx
|
|
102
|
+
var y = 0f
|
|
103
|
+
tree.rowHeights.forEachIndexed { rowIndex, rowHeight ->
|
|
104
|
+
val isHeader = table.rows.getOrNull(rowIndex)?.isHeader == true
|
|
105
|
+
canvas.save()
|
|
106
|
+
canvas.translate(0f, y)
|
|
107
|
+
BoxDrawing.draw(canvas, table.rowStyle(isHeader), width, rowHeight)
|
|
108
|
+
canvas.restore()
|
|
109
|
+
y += rowHeight
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
|
|
114
|
+
val table = block ?: return
|
|
115
|
+
val tree = measured ?: return
|
|
116
|
+
|
|
117
|
+
var index = 0
|
|
118
|
+
var y = 0f
|
|
119
|
+
tree.cellLayouts.forEachIndexed { rowIndex, row ->
|
|
120
|
+
var x = 0f
|
|
121
|
+
val isHeader = table.rows.getOrNull(rowIndex)?.isHeader == true
|
|
122
|
+
row.forEachIndexed { column, cell ->
|
|
123
|
+
val child = getChildAt(index++) ?: return@forEachIndexed
|
|
124
|
+
val cellLeft = (x + table.padLeft(isHeader)).toInt()
|
|
125
|
+
val cellTop =
|
|
126
|
+
(y + table.padTop(isHeader) + table.rowStyle(isHeader).borderTopWidth).toInt()
|
|
127
|
+
child.measure(
|
|
128
|
+
MeasureSpec.makeMeasureSpec(cell.width, MeasureSpec.EXACTLY),
|
|
129
|
+
MeasureSpec.makeMeasureSpec(cell.height, MeasureSpec.EXACTLY),
|
|
130
|
+
)
|
|
131
|
+
child.layout(cellLeft, cellTop, cellLeft + cell.width, cellTop + cell.height)
|
|
132
|
+
x += tree.columnWidths[column]
|
|
133
|
+
}
|
|
134
|
+
y += tree.rowHeights[rowIndex]
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
override fun shouldDelayChildPressedState(): Boolean = false
|
|
139
|
+
}
|
package/cpp/core/Ast.h
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <cstdint>
|
|
4
|
+
#include <memory>
|
|
5
|
+
#include <string>
|
|
6
|
+
#include <vector>
|
|
7
|
+
|
|
8
|
+
namespace jetmarkdown {
|
|
9
|
+
|
|
10
|
+
enum class NodeType : uint8_t {
|
|
11
|
+
Document = 0,
|
|
12
|
+
Heading = 1,
|
|
13
|
+
Paragraph = 2,
|
|
14
|
+
BlockQuote = 3,
|
|
15
|
+
CodeBlock = 4,
|
|
16
|
+
List = 5,
|
|
17
|
+
ListItem = 6,
|
|
18
|
+
Table = 7,
|
|
19
|
+
TableRow = 8,
|
|
20
|
+
TableCell = 9,
|
|
21
|
+
Image = 10,
|
|
22
|
+
ThematicBreak = 11,
|
|
23
|
+
Text = 12,
|
|
24
|
+
SoftBreak = 13,
|
|
25
|
+
HardBreak = 14,
|
|
26
|
+
Bold = 15,
|
|
27
|
+
Italic = 16,
|
|
28
|
+
Strikethrough = 17,
|
|
29
|
+
Link = 18,
|
|
30
|
+
InlineCode = 19,
|
|
31
|
+
Spoiler = 20,
|
|
32
|
+
Superscript = 21,
|
|
33
|
+
Subscript = 22,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
enum class CellAlign : uint8_t {
|
|
37
|
+
Default = 0,
|
|
38
|
+
Left = 1,
|
|
39
|
+
Center = 2,
|
|
40
|
+
Right = 3,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Node field usage by type:
|
|
44
|
+
// Text: text = content
|
|
45
|
+
// Image: text = alt text, url = source
|
|
46
|
+
// CodeBlock: text = code content, url = info string (language)
|
|
47
|
+
// InlineCode: text = content
|
|
48
|
+
// Link: url = destination, children = label
|
|
49
|
+
// Heading: level = 1..6
|
|
50
|
+
// List: ordered, startIndex
|
|
51
|
+
// TableRow: level = 1 when header row
|
|
52
|
+
// TableCell: level = CellAlign
|
|
53
|
+
struct Node {
|
|
54
|
+
NodeType type = NodeType::Document;
|
|
55
|
+
std::string text;
|
|
56
|
+
std::string url;
|
|
57
|
+
uint8_t level = 0;
|
|
58
|
+
bool ordered = false;
|
|
59
|
+
// Text nodes born from character escapes/entities. The inline-extension
|
|
60
|
+
// scanner must not read delimiters out of them (the author explicitly
|
|
61
|
+
// de-fanged the characters), so they never merge with neighbours.
|
|
62
|
+
bool verbatim = false;
|
|
63
|
+
int32_t startIndex = 1;
|
|
64
|
+
std::vector<Node*> children;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Owns every node of one parsed document; nodes are freed together.
|
|
68
|
+
class AstArena {
|
|
69
|
+
public:
|
|
70
|
+
Node* alloc(NodeType type) {
|
|
71
|
+
nodes_.push_back(std::make_unique<Node>());
|
|
72
|
+
nodes_.back()->type = type;
|
|
73
|
+
return nodes_.back().get();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private:
|
|
77
|
+
std::vector<std::unique_ptr<Node>> nodes_;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
struct MarkdownDocument {
|
|
81
|
+
AstArena arena;
|
|
82
|
+
Node* root = nullptr;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
} // namespace jetmarkdown
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#include "AstJson.h"
|
|
2
|
+
|
|
3
|
+
#include <cstdio>
|
|
4
|
+
|
|
5
|
+
namespace jetmarkdown {
|
|
6
|
+
|
|
7
|
+
namespace {
|
|
8
|
+
|
|
9
|
+
const char* typeName(NodeType type) {
|
|
10
|
+
switch (type) {
|
|
11
|
+
case NodeType::Document: return "document";
|
|
12
|
+
case NodeType::Heading: return "heading";
|
|
13
|
+
case NodeType::Paragraph: return "paragraph";
|
|
14
|
+
case NodeType::BlockQuote: return "blockQuote";
|
|
15
|
+
case NodeType::CodeBlock: return "codeBlock";
|
|
16
|
+
case NodeType::List: return "list";
|
|
17
|
+
case NodeType::ListItem: return "listItem";
|
|
18
|
+
case NodeType::Table: return "table";
|
|
19
|
+
case NodeType::TableRow: return "tableRow";
|
|
20
|
+
case NodeType::TableCell: return "tableCell";
|
|
21
|
+
case NodeType::Image: return "image";
|
|
22
|
+
case NodeType::ThematicBreak: return "thematicBreak";
|
|
23
|
+
case NodeType::Text: return "text";
|
|
24
|
+
case NodeType::SoftBreak: return "softBreak";
|
|
25
|
+
case NodeType::HardBreak: return "hardBreak";
|
|
26
|
+
case NodeType::Bold: return "bold";
|
|
27
|
+
case NodeType::Italic: return "italic";
|
|
28
|
+
case NodeType::Strikethrough: return "strikethrough";
|
|
29
|
+
case NodeType::Link: return "link";
|
|
30
|
+
case NodeType::InlineCode: return "inlineCode";
|
|
31
|
+
case NodeType::Spoiler: return "spoiler";
|
|
32
|
+
case NodeType::Superscript: return "superscript";
|
|
33
|
+
case NodeType::Subscript: return "subscript";
|
|
34
|
+
}
|
|
35
|
+
return "unknown";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
void appendEscaped(std::string& out, const std::string& value) {
|
|
39
|
+
for (char c : value) {
|
|
40
|
+
switch (c) {
|
|
41
|
+
case '"': out += "\\\""; break;
|
|
42
|
+
case '\\': out += "\\\\"; break;
|
|
43
|
+
case '\n': out += "\\n"; break;
|
|
44
|
+
case '\t': out += "\\t"; break;
|
|
45
|
+
case '\r': out += "\\r"; break;
|
|
46
|
+
default:
|
|
47
|
+
if (static_cast<unsigned char>(c) < 0x20) {
|
|
48
|
+
char buf[8];
|
|
49
|
+
std::snprintf(buf, sizeof(buf), "\\u%04x", c);
|
|
50
|
+
out += buf;
|
|
51
|
+
} else {
|
|
52
|
+
out += c;
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
void write(std::string& out, const Node* node) {
|
|
60
|
+
out += "{\"type\":\"";
|
|
61
|
+
out += typeName(node->type);
|
|
62
|
+
out += '"';
|
|
63
|
+
if (!node->text.empty()) {
|
|
64
|
+
out += ",\"text\":\"";
|
|
65
|
+
appendEscaped(out, node->text);
|
|
66
|
+
out += '"';
|
|
67
|
+
}
|
|
68
|
+
if (!node->url.empty()) {
|
|
69
|
+
out += ",\"url\":\"";
|
|
70
|
+
appendEscaped(out, node->url);
|
|
71
|
+
out += '"';
|
|
72
|
+
}
|
|
73
|
+
if (node->level != 0) {
|
|
74
|
+
out += ",\"level\":";
|
|
75
|
+
out += std::to_string(node->level);
|
|
76
|
+
}
|
|
77
|
+
if (node->type == NodeType::List) {
|
|
78
|
+
out += ",\"ordered\":";
|
|
79
|
+
out += node->ordered ? "true" : "false";
|
|
80
|
+
if (node->ordered && node->startIndex != 1) {
|
|
81
|
+
out += ",\"start\":";
|
|
82
|
+
out += std::to_string(node->startIndex);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!node->children.empty()) {
|
|
86
|
+
out += ",\"children\":[";
|
|
87
|
+
bool first = true;
|
|
88
|
+
for (size_t i = 0; i < node->children.size(); i++) {
|
|
89
|
+
// Adjacent text nodes dump as one: verbatim (entity-born) nodes are a
|
|
90
|
+
// scanner-facing distinction, not a content one, and tests compare
|
|
91
|
+
// content.
|
|
92
|
+
if (node->children[i]->type == NodeType::Text) {
|
|
93
|
+
std::string merged = node->children[i]->text;
|
|
94
|
+
while (i + 1 < node->children.size() &&
|
|
95
|
+
node->children[i + 1]->type == NodeType::Text) {
|
|
96
|
+
merged += node->children[i + 1]->text;
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
if (!first) {
|
|
100
|
+
out += ',';
|
|
101
|
+
}
|
|
102
|
+
first = false;
|
|
103
|
+
out += "{\"type\":\"text\",\"text\":\"";
|
|
104
|
+
appendEscaped(out, merged);
|
|
105
|
+
out += "\"}";
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!first) {
|
|
109
|
+
out += ',';
|
|
110
|
+
}
|
|
111
|
+
first = false;
|
|
112
|
+
write(out, node->children[i]);
|
|
113
|
+
}
|
|
114
|
+
out += ']';
|
|
115
|
+
}
|
|
116
|
+
out += '}';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
} // namespace
|
|
120
|
+
|
|
121
|
+
std::string astToJson(const Node* node) {
|
|
122
|
+
std::string out;
|
|
123
|
+
if (node != nullptr) {
|
|
124
|
+
write(out, node);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
} // namespace jetmarkdown
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#include "AstSerializer.h"
|
|
2
|
+
|
|
3
|
+
namespace jetmarkdown {
|
|
4
|
+
|
|
5
|
+
namespace {
|
|
6
|
+
|
|
7
|
+
void writeU32(std::vector<uint8_t>& out, uint32_t value) {
|
|
8
|
+
out.push_back(static_cast<uint8_t>(value & 0xFF));
|
|
9
|
+
out.push_back(static_cast<uint8_t>((value >> 8) & 0xFF));
|
|
10
|
+
out.push_back(static_cast<uint8_t>((value >> 16) & 0xFF));
|
|
11
|
+
out.push_back(static_cast<uint8_t>((value >> 24) & 0xFF));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
void writeNode(std::vector<uint8_t>& out, const Node* node) {
|
|
15
|
+
out.push_back(static_cast<uint8_t>(node->type));
|
|
16
|
+
out.push_back(node->level);
|
|
17
|
+
out.push_back(node->ordered ? 1 : 0);
|
|
18
|
+
writeU32(out, static_cast<uint32_t>(node->startIndex));
|
|
19
|
+
writeU32(out, static_cast<uint32_t>(node->text.size()));
|
|
20
|
+
out.insert(out.end(), node->text.begin(), node->text.end());
|
|
21
|
+
writeU32(out, static_cast<uint32_t>(node->url.size()));
|
|
22
|
+
out.insert(out.end(), node->url.begin(), node->url.end());
|
|
23
|
+
writeU32(out, static_cast<uint32_t>(node->children.size()));
|
|
24
|
+
for (const Node* child : node->children) {
|
|
25
|
+
writeNode(out, child);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
} // namespace
|
|
30
|
+
|
|
31
|
+
std::vector<uint8_t> serializeAst(const Node* root) {
|
|
32
|
+
std::vector<uint8_t> out;
|
|
33
|
+
if (root != nullptr) {
|
|
34
|
+
out.reserve(1024);
|
|
35
|
+
writeNode(out, root);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
} // namespace jetmarkdown
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <cstdint>
|
|
4
|
+
#include <vector>
|
|
5
|
+
|
|
6
|
+
#include "Ast.h"
|
|
7
|
+
|
|
8
|
+
namespace jetmarkdown {
|
|
9
|
+
|
|
10
|
+
// Flat little-endian pre-order encoding, decoded by AstDecoder.kt on Android.
|
|
11
|
+
// Per node:
|
|
12
|
+
// u8 type
|
|
13
|
+
// u8 level
|
|
14
|
+
// u8 flags (bit 0: ordered)
|
|
15
|
+
// i32 startIndex
|
|
16
|
+
// u32 textLen, textLen bytes (UTF-8)
|
|
17
|
+
// u32 urlLen, urlLen bytes (UTF-8)
|
|
18
|
+
// u32 childCount, then children recursively
|
|
19
|
+
std::vector<uint8_t> serializeAst(const Node* root);
|
|
20
|
+
|
|
21
|
+
} // namespace jetmarkdown
|