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.
Files changed (157) hide show
  1. package/JetMarkdown.podspec +42 -0
  2. package/LICENSE +20 -0
  3. package/README.md +247 -0
  4. package/android/CMakeLists.txt +52 -0
  5. package/android/build.gradle +55 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/JniBindings.cpp +243 -0
  8. package/android/src/main/java/com/jetmarkdown/JetMarkdownNative.kt +114 -0
  9. package/android/src/main/java/com/jetmarkdown/JetMarkdownPackage.kt +22 -0
  10. package/android/src/main/java/com/jetmarkdown/JetMarkdownView.kt +235 -0
  11. package/android/src/main/java/com/jetmarkdown/JetMarkdownViewManager.kt +75 -0
  12. package/android/src/main/java/com/jetmarkdown/editor/EditorSpans.kt +217 -0
  13. package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorManager.kt +224 -0
  14. package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorView.kt +1736 -0
  15. package/android/src/main/java/com/jetmarkdown/measure/MarkdownMeasurer.kt +54 -0
  16. package/android/src/main/java/com/jetmarkdown/parser/AstDecoder.kt +59 -0
  17. package/android/src/main/java/com/jetmarkdown/parser/MdNode.kt +43 -0
  18. package/android/src/main/java/com/jetmarkdown/render/ContentCache.kt +53 -0
  19. package/android/src/main/java/com/jetmarkdown/render/RenderedContent.kt +318 -0
  20. package/android/src/main/java/com/jetmarkdown/render/SpannableRenderer.kt +658 -0
  21. package/android/src/main/java/com/jetmarkdown/render/spans/ChipSpan.kt +17 -0
  22. package/android/src/main/java/com/jetmarkdown/render/spans/InteractiveSpans.kt +11 -0
  23. package/android/src/main/java/com/jetmarkdown/render/spans/MarkdownLineHeightSpan.kt +59 -0
  24. package/android/src/main/java/com/jetmarkdown/render/spans/RunSpan.kt +40 -0
  25. package/android/src/main/java/com/jetmarkdown/style/Fonts.kt +34 -0
  26. package/android/src/main/java/com/jetmarkdown/style/LayoutStyleSpec.kt +71 -0
  27. package/android/src/main/java/com/jetmarkdown/style/PlatformColorResolver.kt +77 -0
  28. package/android/src/main/java/com/jetmarkdown/style/StyleConfig.kt +80 -0
  29. package/android/src/main/java/com/jetmarkdown/style/TextStyleSpec.kt +61 -0
  30. package/android/src/main/java/com/jetmarkdown/views/BlockStackView.kt +292 -0
  31. package/android/src/main/java/com/jetmarkdown/views/BlockTextView.kt +378 -0
  32. package/android/src/main/java/com/jetmarkdown/views/BoxDrawing.kt +50 -0
  33. package/android/src/main/java/com/jetmarkdown/views/MarkdownHost.kt +12 -0
  34. package/android/src/main/java/com/jetmarkdown/views/MarkdownImageView.kt +113 -0
  35. package/android/src/main/java/com/jetmarkdown/views/NestedHorizontalScrollView.kt +87 -0
  36. package/android/src/main/java/com/jetmarkdown/views/TableBlockView.kt +139 -0
  37. package/cpp/core/Ast.h +85 -0
  38. package/cpp/core/AstJson.cpp +129 -0
  39. package/cpp/core/AstJson.h +12 -0
  40. package/cpp/core/AstSerializer.cpp +40 -0
  41. package/cpp/core/AstSerializer.h +21 -0
  42. package/cpp/core/AstToMarkdown.cpp +549 -0
  43. package/cpp/core/AstToMarkdown.h +16 -0
  44. package/cpp/core/EditorRuns.cpp +640 -0
  45. package/cpp/core/EditorRuns.h +94 -0
  46. package/cpp/core/EditorText.cpp +15 -0
  47. package/cpp/core/EditorText.h +18 -0
  48. package/cpp/core/InlineExtensions.cpp +305 -0
  49. package/cpp/core/InlineExtensions.h +17 -0
  50. package/cpp/core/Parser.cpp +470 -0
  51. package/cpp/core/Parser.h +16 -0
  52. package/cpp/core/Preprocess.cpp +181 -0
  53. package/cpp/core/Preprocess.h +14 -0
  54. package/cpp/md4c/LICENSE +22 -0
  55. package/cpp/md4c/VERSION +1 -0
  56. package/cpp/md4c/md4c.c +6462 -0
  57. package/cpp/md4c/md4c.h +407 -0
  58. package/cpp/react/JetMarkdownEditorShadowNode.cpp +54 -0
  59. package/cpp/react/JetMarkdownEditorShadowNode.h +55 -0
  60. package/cpp/react/JetMarkdownEditorState.h +35 -0
  61. package/cpp/react/JetMarkdownMeasurer.cpp +32 -0
  62. package/cpp/react/JetMarkdownMeasurer.h +41 -0
  63. package/cpp/react/JetMarkdownShadowNode.cpp +100 -0
  64. package/cpp/react/JetMarkdownShadowNode.h +53 -0
  65. package/cpp/react/JetMarkdownState.h +52 -0
  66. package/cpp/react/override/react/renderer/components/JetMarkdownViewSpec/ComponentDescriptors.h +21 -0
  67. package/cpp/tests/parser_tests.cpp +662 -0
  68. package/cpp/tests/run_tests.sh +25 -0
  69. package/ios/JetMarkdownView.h +14 -0
  70. package/ios/JetMarkdownView.mm +468 -0
  71. package/ios/editor/JetMarkdownEditor.h +12 -0
  72. package/ios/editor/JetMarkdownEditor.mm +2002 -0
  73. package/ios/measure/JMDMarkdownMeasurer.h +22 -0
  74. package/ios/measure/JMDMarkdownMeasurer.mm +38 -0
  75. package/ios/render/JMDBlock.h +97 -0
  76. package/ios/render/JMDBlock.m +28 -0
  77. package/ios/render/JMDBlockRenderer.h +17 -0
  78. package/ios/render/JMDBlockRenderer.mm +852 -0
  79. package/ios/render/JMDContentCache.h +17 -0
  80. package/ios/render/JMDContentCache.mm +42 -0
  81. package/ios/render/JMDRenderedContent.h +35 -0
  82. package/ios/render/JMDRenderedContent.mm +274 -0
  83. package/ios/style/JMDFontScale.h +33 -0
  84. package/ios/style/JMDLayoutStyle.h +37 -0
  85. package/ios/style/JMDLayoutStyle.mm +68 -0
  86. package/ios/style/JMDStyleConfig.h +39 -0
  87. package/ios/style/JMDStyleConfig.mm +111 -0
  88. package/ios/style/JMDTextStyle.h +25 -0
  89. package/ios/style/JMDTextStyle.mm +68 -0
  90. package/ios/views/JMDBlockStackView.h +18 -0
  91. package/ios/views/JMDBlockStackView.m +97 -0
  92. package/ios/views/JMDBlockTextView.h +24 -0
  93. package/ios/views/JMDBlockTextView.m +340 -0
  94. package/ios/views/JMDBoxViews.h +33 -0
  95. package/ios/views/JMDBoxViews.m +292 -0
  96. package/ios/views/JMDImageView.h +19 -0
  97. package/ios/views/JMDImageView.m +68 -0
  98. package/ios/views/JMDMarkdownHost.h +16 -0
  99. package/ios/views/JMDTableView.h +16 -0
  100. package/ios/views/JMDTableView.m +172 -0
  101. package/lib/module/JetMarkdownEditor.js +85 -0
  102. package/lib/module/JetMarkdownEditor.js.map +1 -0
  103. package/lib/module/JetMarkdownEditor.native.js +244 -0
  104. package/lib/module/JetMarkdownEditor.native.js.map +1 -0
  105. package/lib/module/JetMarkdownEditorNativeComponent.ts +173 -0
  106. package/lib/module/JetMarkdownView.js +19 -0
  107. package/lib/module/JetMarkdownView.js.map +1 -0
  108. package/lib/module/JetMarkdownView.native.js +44 -0
  109. package/lib/module/JetMarkdownView.native.js.map +1 -0
  110. package/lib/module/JetMarkdownViewNativeComponent.ts +35 -0
  111. package/lib/module/defaultStyles.js +122 -0
  112. package/lib/module/defaultStyles.js.map +1 -0
  113. package/lib/module/index.js +9 -0
  114. package/lib/module/index.js.map +1 -0
  115. package/lib/module/package.json +1 -0
  116. package/lib/module/serializeStyles.js +259 -0
  117. package/lib/module/serializeStyles.js.map +1 -0
  118. package/lib/module/types.js +4 -0
  119. package/lib/module/types.js.map +1 -0
  120. package/lib/module/useJetMarkdownEditor.js +41 -0
  121. package/lib/module/useJetMarkdownEditor.js.map +1 -0
  122. package/lib/typescript/package.json +1 -0
  123. package/lib/typescript/src/JetMarkdownEditor.d.ts +6 -0
  124. package/lib/typescript/src/JetMarkdownEditor.d.ts.map +1 -0
  125. package/lib/typescript/src/JetMarkdownEditor.native.d.ts +6 -0
  126. package/lib/typescript/src/JetMarkdownEditor.native.d.ts.map +1 -0
  127. package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts +105 -0
  128. package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts.map +1 -0
  129. package/lib/typescript/src/JetMarkdownView.d.ts +3 -0
  130. package/lib/typescript/src/JetMarkdownView.d.ts.map +1 -0
  131. package/lib/typescript/src/JetMarkdownView.native.d.ts +3 -0
  132. package/lib/typescript/src/JetMarkdownView.native.d.ts.map +1 -0
  133. package/lib/typescript/src/JetMarkdownViewNativeComponent.d.ts +28 -0
  134. package/lib/typescript/src/JetMarkdownViewNativeComponent.d.ts.map +1 -0
  135. package/lib/typescript/src/defaultStyles.d.ts +20 -0
  136. package/lib/typescript/src/defaultStyles.d.ts.map +1 -0
  137. package/lib/typescript/src/index.d.ts +7 -0
  138. package/lib/typescript/src/index.d.ts.map +1 -0
  139. package/lib/typescript/src/serializeStyles.d.ts +34 -0
  140. package/lib/typescript/src/serializeStyles.d.ts.map +1 -0
  141. package/lib/typescript/src/types.d.ts +352 -0
  142. package/lib/typescript/src/types.d.ts.map +1 -0
  143. package/lib/typescript/src/useJetMarkdownEditor.d.ts +36 -0
  144. package/lib/typescript/src/useJetMarkdownEditor.d.ts.map +1 -0
  145. package/package.json +150 -0
  146. package/react-native.config.js +11 -0
  147. package/src/JetMarkdownEditor.native.tsx +301 -0
  148. package/src/JetMarkdownEditor.tsx +90 -0
  149. package/src/JetMarkdownEditorNativeComponent.ts +173 -0
  150. package/src/JetMarkdownView.native.tsx +67 -0
  151. package/src/JetMarkdownView.tsx +16 -0
  152. package/src/JetMarkdownViewNativeComponent.ts +35 -0
  153. package/src/defaultStyles.ts +102 -0
  154. package/src/index.tsx +34 -0
  155. package/src/serializeStyles.ts +369 -0
  156. package/src/types.ts +438 -0
  157. 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,12 @@
1
+ #pragma once
2
+
3
+ #include <string>
4
+
5
+ #include "Ast.h"
6
+
7
+ namespace jetmarkdown {
8
+
9
+ // Compact JSON rendering of the AST for golden tests and debugging.
10
+ std::string astToJson(const Node* node);
11
+
12
+ } // 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