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,114 @@
1
+ package com.jetmarkdown
2
+
3
+ import com.jetmarkdown.measure.MarkdownMeasurer
4
+ import com.jetmarkdown.parser.AstDecoder
5
+ import com.jetmarkdown.parser.MdNode
6
+
7
+ /**
8
+ * JNI bridge into the shared C++ core. The natives are linked into the app's
9
+ * `libappmodules.so` (loaded by React Native at startup), so no explicit
10
+ * `System.loadLibrary` is required.
11
+ */
12
+ object JetMarkdownNative {
13
+ @Volatile private var installed = false
14
+
15
+ fun ensureInstalled() {
16
+ if (!installed) {
17
+ synchronized(this) {
18
+ if (!installed) {
19
+ // libappmodules.so may not be loaded yet at package-construction
20
+ // time; callers retry from later lifecycle points.
21
+ installed = runCatching { installMeasurer(MarkdownMeasurer) }.isSuccess
22
+ }
23
+ }
24
+ }
25
+ }
26
+
27
+ fun parseMarkdown(markdown: String): MdNode {
28
+ return AstDecoder.decode(parse(markdown.toByteArray(Charsets.UTF_8)))
29
+ }
30
+
31
+
32
+ /** Decoded editor content: text, inline-mark runs, line blocks, links. */
33
+ class EditorContent(
34
+ val text: String,
35
+ /** Flat `[start, end, flags]` triples in UTF-16 offsets. */
36
+ val runs: IntArray,
37
+ /** Flat `[type, level]` pairs, one per text line. */
38
+ val lineBlocks: IntArray,
39
+ /** Flat `[start, end]` pairs, aligned with [linkUrls]. */
40
+ val linkRanges: IntArray,
41
+ val linkUrls: List<String>,
42
+ )
43
+
44
+ /**
45
+ * Editor: markdown from text + inline-mark runs + per-line blocks +
46
+ * links. Runs are flat `[start, end, flags]` triples in UTF-16 offsets
47
+ * (Spannable indices); lineBlocks are `[type, level]` pairs per line;
48
+ * links are `[start, end]` pairs with URLs joined by '\n'.
49
+ */
50
+ fun markdownFromEditor(
51
+ text: String,
52
+ runs: IntArray,
53
+ lineBlocks: IntArray,
54
+ linkRanges: IntArray = IntArray(0),
55
+ linkUrls: List<String> = emptyList(),
56
+ ): String {
57
+ return markdownFromEditorContent(
58
+ text.toByteArray(Charsets.UTF_8),
59
+ runs,
60
+ lineBlocks,
61
+ linkRanges,
62
+ linkUrls.joinToString("\n").toByteArray(Charsets.UTF_8),
63
+ ).toString(Charsets.UTF_8)
64
+ }
65
+
66
+ /**
67
+ * Editor: markdown parsed into editor content. The native payload is
68
+ * `[int32 runCount][triples][int32 lineCount][pairs][int32 linkCount]
69
+ * [(start, end, urlLen) triples][url bytes][utf8 text]`, little-endian.
70
+ */
71
+ fun editorFromMarkdown(markdown: String): EditorContent {
72
+ val bytes = editorFromMarkdownContent(markdown.toByteArray(Charsets.UTF_8))
73
+ val buffer = java.nio.ByteBuffer.wrap(bytes).order(java.nio.ByteOrder.LITTLE_ENDIAN)
74
+ val runs = IntArray(buffer.int * 3)
75
+ for (i in runs.indices) {
76
+ runs[i] = buffer.int
77
+ }
78
+ val lineBlocks = IntArray(buffer.int * 2)
79
+ for (i in lineBlocks.indices) {
80
+ lineBlocks[i] = buffer.int
81
+ }
82
+ val linkCount = buffer.int
83
+ val linkRanges = IntArray(linkCount * 2)
84
+ val urlLengths = IntArray(linkCount)
85
+ for (i in 0 until linkCount) {
86
+ linkRanges[i * 2] = buffer.int
87
+ linkRanges[i * 2 + 1] = buffer.int
88
+ urlLengths[i] = buffer.int
89
+ }
90
+ val linkUrls = ArrayList<String>(linkCount)
91
+ for (i in 0 until linkCount) {
92
+ val urlBytes = ByteArray(urlLengths[i])
93
+ buffer.get(urlBytes)
94
+ linkUrls.add(urlBytes.toString(Charsets.UTF_8))
95
+ }
96
+ val text = ByteArray(buffer.remaining())
97
+ buffer.get(text)
98
+ return EditorContent(text.toString(Charsets.UTF_8), runs, lineBlocks, linkRanges, linkUrls)
99
+ }
100
+
101
+ @JvmStatic private external fun parse(markdown: ByteArray): ByteArray
102
+
103
+ @JvmStatic private external fun markdownFromEditorContent(
104
+ text: ByteArray,
105
+ runs: IntArray,
106
+ lineBlocks: IntArray,
107
+ linkRanges: IntArray,
108
+ linkUrls: ByteArray,
109
+ ): ByteArray
110
+
111
+ @JvmStatic private external fun editorFromMarkdownContent(markdown: ByteArray): ByteArray
112
+
113
+ @JvmStatic private external fun installMeasurer(measurer: MarkdownMeasurer)
114
+ }
@@ -0,0 +1,22 @@
1
+ package com.jetmarkdown
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfoProvider
7
+ import com.facebook.react.uimanager.ViewManager
8
+ import com.jetmarkdown.editor.JetMarkdownEditorManager
9
+
10
+ class JetMarkdownViewPackage : BaseReactPackage() {
11
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
12
+ // Fabric measures shadow nodes before the first view instance exists;
13
+ // font/color resolution must work from the very first measure. The view
14
+ // managers re-install with the themed context on view creation.
15
+ com.jetmarkdown.style.PlatformColorResolver.install(reactContext)
16
+ return listOf(JetMarkdownViewManager(), JetMarkdownEditorManager())
17
+ }
18
+
19
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
20
+
21
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider { emptyMap() }
22
+ }
@@ -0,0 +1,235 @@
1
+ package com.jetmarkdown
2
+
3
+ import android.content.Context
4
+ import android.graphics.Color
5
+ import android.view.ViewGroup
6
+ import com.facebook.react.bridge.Arguments
7
+ import com.facebook.react.bridge.ReadableArray
8
+ import com.facebook.react.bridge.WritableMap
9
+ import com.facebook.react.uimanager.StateWrapper
10
+ import com.facebook.react.uimanager.UIManagerHelper
11
+ import com.facebook.react.uimanager.events.Event
12
+ import com.jetmarkdown.render.ContentCache
13
+ import com.jetmarkdown.style.StyleConfig
14
+ import com.jetmarkdown.views.BlockStackView
15
+ import com.jetmarkdown.views.MarkdownHost
16
+
17
+ /**
18
+ * Host view: one nested block stack. Fabric supplies the final frame (the
19
+ * C++ shadow node measured the same cached content), so onLayout only
20
+ * distributes frames.
21
+ */
22
+ class JetMarkdownView(context: Context) : ViewGroup(context), MarkdownHost {
23
+ private var markdown: String = ""
24
+ private var stylesJson: String = ""
25
+
26
+ var allowFontScaling = true
27
+ set(value) {
28
+ if (field != value) {
29
+ field = value
30
+ boundKey = null
31
+ requestLayout()
32
+ }
33
+ }
34
+ private var boundKey: List<Any>? = null
35
+ private var boundWidth: Int = 0
36
+ private val stack = BlockStackView(context)
37
+
38
+ /** url -> [w, h] dp: from the images prop (wins) and loaded bitmaps. */
39
+ private val propImageSizes = HashMap<String, FloatArray>()
40
+ private val loadedImageSizes = HashMap<String, FloatArray>()
41
+ var stateWrapper: StateWrapper? = null
42
+
43
+ private val revealedSpoilers = HashSet<Int>()
44
+
45
+ init {
46
+ addView(stack)
47
+ stack.host = this
48
+ }
49
+
50
+ /** Fabric view recycling: clear all per-content state. */
51
+ fun resetForRecycle() {
52
+ markdown = ""
53
+ stylesJson = ""
54
+ allowFontScaling = true
55
+ stateWrapper = null
56
+ boundKey = null
57
+ boundWidth = 0
58
+ propImageSizes.clear()
59
+ loadedImageSizes.clear()
60
+ revealedSpoilers.clear()
61
+ stack.setBlocks(emptyList(), 0f)
62
+ }
63
+
64
+ override fun onImageIntrinsicSize(url: String, widthDp: Float, heightDp: Float) {
65
+ if (!propImageSizes.containsKey(url) && !loadedImageSizes.containsKey(url)) {
66
+ loadedImageSizes[url] = floatArrayOf(widthDp, heightDp)
67
+ publishImageSizes()
68
+ requestLayout()
69
+ }
70
+ }
71
+
72
+ override fun isSpoilerRevealed(id: Int): Boolean = revealedSpoilers.contains(id)
73
+
74
+ override fun toggleSpoiler(id: Int) {
75
+ if (!revealedSpoilers.add(id)) {
76
+ revealedSpoilers.remove(id)
77
+ }
78
+ invalidateDeep(this)
79
+ }
80
+
81
+ override fun onLinkPress(url: String) = emitUrlEvent("topLinkPress", url)
82
+
83
+ override fun onLinkLongPress(url: String) = emitUrlEvent("topLinkLongPress", url)
84
+
85
+ override fun onImagePress(url: String, xDp: Float, yDp: Float, widthDp: Float, heightDp: Float) =
86
+ emitEvent("topImagePress") {
87
+ putString("url", url)
88
+ putDouble("x", xDp.toDouble())
89
+ putDouble("y", yDp.toDouble())
90
+ putDouble("width", widthDp.toDouble())
91
+ putDouble("height", heightDp.toDouble())
92
+ }
93
+
94
+ private fun emitUrlEvent(name: String, url: String) =
95
+ emitEvent(name) { putString("url", url) }
96
+
97
+ private fun emitEvent(name: String, data: WritableMap.() -> Unit) {
98
+ val reactContext = context as? com.facebook.react.bridge.ReactContext ?: return
99
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id) ?: return
100
+ val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
101
+ dispatcher.dispatchEvent(object : Event<Nothing>(surfaceId, id) {
102
+ override fun getEventName(): String = name
103
+
104
+ override fun getEventData(): WritableMap = Arguments.createMap().apply(data)
105
+ })
106
+ }
107
+
108
+ private fun invalidateDeep(view: android.view.View) {
109
+ view.invalidate()
110
+ if (view is ViewGroup) {
111
+ for (i in 0 until view.childCount) {
112
+ invalidateDeep(view.getChildAt(i))
113
+ }
114
+ }
115
+ }
116
+
117
+ fun setImages(value: ReadableArray?) {
118
+ propImageSizes.clear()
119
+ if (value != null) {
120
+ for (i in 0 until value.size()) {
121
+ val entry = value.getMap(i) ?: continue
122
+ val url = entry.getString("url") ?: continue
123
+ propImageSizes[url] = floatArrayOf(
124
+ entry.getDouble("width").toFloat(),
125
+ entry.getDouble("height").toFloat(),
126
+ )
127
+ }
128
+ }
129
+ requestLayout()
130
+ invalidate()
131
+ }
132
+
133
+ // Pushes discovered sizes into the shadow-node state so measure() grows
134
+ // the component. The prop entries stay out (already known to C++).
135
+ private fun publishImageSizes() {
136
+ val wrapper = stateWrapper ?: return
137
+ val sizes = Arguments.createMap()
138
+ for ((url, size) in loadedImageSizes) {
139
+ val entry = Arguments.createMap()
140
+ entry.putDouble("width", size[0].toDouble())
141
+ entry.putDouble("height", size[1].toDouble())
142
+ sizes.putMap(url, entry)
143
+ }
144
+ val state = Arguments.createMap()
145
+ state.putMap("imageSizes", sizes)
146
+ wrapper.updateState(state)
147
+ }
148
+
149
+ private fun mergedImageSizes(): Map<String, FloatArray> {
150
+ if (loadedImageSizes.isEmpty() && propImageSizes.isEmpty()) {
151
+ return emptyMap()
152
+ }
153
+ val merged = HashMap<String, FloatArray>(loadedImageSizes)
154
+ merged.putAll(propImageSizes)
155
+ return merged
156
+ }
157
+
158
+ fun setMarkdown(value: String?) {
159
+ val next = value ?: ""
160
+ if (next != markdown) {
161
+ markdown = next
162
+ // Spoiler ids are render-order counters; carrying revealed indices
163
+ // into different content would pre-reveal the wrong spans.
164
+ revealedSpoilers.clear()
165
+ // Sizes learned for the old document may not apply to the new one.
166
+ loadedImageSizes.clear()
167
+ requestLayout()
168
+ invalidate()
169
+ }
170
+ }
171
+
172
+ fun setStylesJson(value: String?) {
173
+ val next = value ?: ""
174
+ if (next != stylesJson) {
175
+ stylesJson = next
176
+ requestLayout()
177
+ invalidate()
178
+ }
179
+ }
180
+
181
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
182
+ // Fabric passes exact dimensions computed by the shadow node.
183
+ setMeasuredDimension(
184
+ MeasureSpec.getSize(widthMeasureSpec),
185
+ MeasureSpec.getSize(heightMeasureSpec),
186
+ )
187
+ }
188
+
189
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
190
+ val density = resources.displayMetrics.density
191
+ // Must match the shadow node's LayoutContext::fontSizeMultiplier
192
+ // (Android's system font scale).
193
+ val fontScale =
194
+ if (allowFontScaling) resources.configuration.fontScale else 1.0f
195
+ val styles = StyleConfig.from(stylesJson)
196
+
197
+ setBackgroundColor(styles.backgroundColor ?: Color.TRANSPARENT)
198
+
199
+ val paddingLeftPx = (styles.paddingLeft * density).toInt()
200
+ val paddingRightPx = (styles.paddingRight * density).toInt()
201
+ val paddingTopPx = (styles.paddingTop * density).toInt()
202
+ val contentWidthPx = (r - l) - paddingLeftPx - paddingRightPx
203
+ if (contentWidthPx <= 0 || markdown.isEmpty()) {
204
+ stack.setBlocks(emptyList(), 0f)
205
+ boundKey = null
206
+ return
207
+ }
208
+
209
+ val content = ContentCache.get(markdown, stylesJson, fontScale)
210
+ val imageSizes = mergedImageSizes()
211
+ val layout = content.layoutFor(contentWidthPx, imageSizes)
212
+
213
+ val key = listOf(markdown, stylesJson, imageSizes.hashCode(), fontScale)
214
+ if (boundKey != key || boundWidth != contentWidthPx) {
215
+ boundKey = key
216
+ boundWidth = contentWidthPx
217
+ stack.setBlocks(layout.measured, content.gap)
218
+ }
219
+
220
+ val contentHeight =
221
+ (layout.totalHeightPx - (styles.paddingTop + styles.paddingBottom) * density).toInt()
222
+ stack.measure(
223
+ MeasureSpec.makeMeasureSpec(contentWidthPx, MeasureSpec.EXACTLY),
224
+ MeasureSpec.makeMeasureSpec(contentHeight, MeasureSpec.EXACTLY),
225
+ )
226
+ stack.layout(
227
+ paddingLeftPx,
228
+ paddingTopPx,
229
+ paddingLeftPx + contentWidthPx,
230
+ paddingTopPx + contentHeight,
231
+ )
232
+ }
233
+
234
+ override fun shouldDelayChildPressedState(): Boolean = false
235
+ }
@@ -0,0 +1,75 @@
1
+ package com.jetmarkdown
2
+
3
+ import com.jetmarkdown.style.PlatformColorResolver
4
+ import com.facebook.react.bridge.ReadableArray
5
+ import com.facebook.react.bridge.ReadableMap
6
+ import com.facebook.react.module.annotations.ReactModule
7
+ import com.facebook.react.uimanager.SimpleViewManager
8
+ import com.facebook.react.uimanager.StateWrapper
9
+ import com.facebook.react.uimanager.ThemedReactContext
10
+ import com.facebook.react.uimanager.ViewManagerDelegate
11
+ import com.facebook.react.uimanager.annotations.ReactProp
12
+ import com.facebook.react.viewmanagers.JetMarkdownViewManagerDelegate
13
+ import com.facebook.react.viewmanagers.JetMarkdownViewManagerInterface
14
+
15
+ @ReactModule(name = JetMarkdownViewManager.NAME)
16
+ class JetMarkdownViewManager : SimpleViewManager<JetMarkdownView>(),
17
+ JetMarkdownViewManagerInterface<JetMarkdownView> {
18
+ private val delegate: ViewManagerDelegate<JetMarkdownView> =
19
+ JetMarkdownViewManagerDelegate(this)
20
+
21
+ init {
22
+ JetMarkdownNative.ensureInstalled()
23
+ }
24
+
25
+ override fun getDelegate(): ViewManagerDelegate<JetMarkdownView> = delegate
26
+
27
+ override fun getName(): String = NAME
28
+
29
+ public override fun createViewInstance(context: ThemedReactContext): JetMarkdownView {
30
+ JetMarkdownNative.ensureInstalled()
31
+ PlatformColorResolver.install(context)
32
+ return JetMarkdownView(context)
33
+ }
34
+
35
+ @ReactProp(name = "allowFontScaling")
36
+ override fun setAllowFontScaling(view: JetMarkdownView?, value: Boolean) {
37
+ view?.allowFontScaling = value
38
+ }
39
+
40
+ @ReactProp(name = "markdown")
41
+ override fun setMarkdown(view: JetMarkdownView?, value: String?) {
42
+ view?.setMarkdown(value)
43
+ }
44
+
45
+ @ReactProp(name = "stylesJson")
46
+ override fun setStylesJson(view: JetMarkdownView?, value: String?) {
47
+ view?.setStylesJson(value)
48
+ }
49
+
50
+ @ReactProp(name = "images")
51
+ override fun setImages(view: JetMarkdownView?, value: ReadableArray?) {
52
+ view?.setImages(value)
53
+ }
54
+
55
+ override fun prepareToRecycleView(
56
+ reactContext: ThemedReactContext,
57
+ view: JetMarkdownView,
58
+ ): JetMarkdownView {
59
+ view.resetForRecycle()
60
+ return view
61
+ }
62
+
63
+ override fun updateState(
64
+ view: JetMarkdownView,
65
+ props: com.facebook.react.uimanager.ReactStylesDiffMap,
66
+ stateWrapper: StateWrapper?,
67
+ ): Any? {
68
+ view.stateWrapper = stateWrapper
69
+ return null
70
+ }
71
+
72
+ companion object {
73
+ const val NAME = "JetMarkdownView"
74
+ }
75
+ }
@@ -0,0 +1,217 @@
1
+ package com.jetmarkdown.editor
2
+
3
+ import android.graphics.Canvas
4
+ import android.graphics.Paint
5
+ import android.graphics.Typeface
6
+ import android.text.Layout
7
+ import android.text.TextPaint
8
+ import android.text.style.LeadingMarginSpan
9
+ import android.text.style.MetricAffectingSpan
10
+
11
+ /** Inline mark bits; mirrors jetmarkdown::EditorMark in cpp/core/EditorRuns.h. */
12
+ object EditorMarks {
13
+ const val BOLD = 1
14
+ const val ITALIC = 1 shl 1
15
+ const val STRIKETHROUGH = 1 shl 2
16
+ const val INLINE_CODE = 1 shl 3
17
+ const val SPOILER = 1 shl 4
18
+ const val SUPERSCRIPT = 1 shl 5
19
+ const val SUBSCRIPT = 1 shl 6
20
+
21
+ val ALL = intArrayOf(
22
+ BOLD, ITALIC, STRIKETHROUGH, INLINE_CODE, SPOILER, SUPERSCRIPT, SUBSCRIPT,
23
+ )
24
+ }
25
+
26
+ /** Per-line block types; mirrors jetmarkdown::EditorBlockType. */
27
+ object EditorBlocks {
28
+ const val HEADING = 1
29
+ const val QUOTE = 2
30
+ const val CODE = 3
31
+ const val BULLET = 4
32
+ const val ORDERED = 5
33
+
34
+ fun pack(type: Int, level: Int): Int = (type shl 8) or level
35
+
36
+ fun type(packed: Int): Int = packed shr 8
37
+
38
+ fun level(packed: Int): Int = packed and 0xFF
39
+
40
+ }
41
+
42
+ /** Marker for every derived visual span; removed wholesale on rebuild. */
43
+ interface EditorDerivedSpan
44
+
45
+ /**
46
+ * Data-only source of truth for one inline mark over a range. Visual styling
47
+ * is carried by derived spans rebuilt from these after every change.
48
+ */
49
+ class EditorMarkSpan(val mark: Int)
50
+
51
+ /**
52
+ * Data-only linked range. Mentions carry an app-scheme URL and `atomic`
53
+ * (the token edits as one unit).
54
+ */
55
+ class LinkDataSpan(val url: String, val atomic: Boolean)
56
+
57
+ /** Derived link appearance. */
58
+ class LinkDisplaySpan(private val color: Int) :
59
+ android.text.style.CharacterStyle(),
60
+ EditorDerivedSpan {
61
+ override fun updateDrawState(paint: TextPaint) {
62
+ paint.color = color
63
+ paint.isUnderlineText = true
64
+ }
65
+ }
66
+
67
+ /** Derived visual styling for the combined mark flags of a range. */
68
+ class EditorDisplaySpan(private val flags: Int) : MetricAffectingSpan(), EditorDerivedSpan {
69
+ override fun updateMeasureState(paint: TextPaint) {
70
+ apply(paint)
71
+ }
72
+
73
+ override fun updateDrawState(paint: TextPaint) {
74
+ apply(paint)
75
+ if (flags and EditorMarks.STRIKETHROUGH != 0) {
76
+ paint.isStrikeThruText = true
77
+ }
78
+ if (flags and EditorMarks.INLINE_CODE != 0) {
79
+ paint.bgColor = 0x26808080
80
+ }
81
+ if (flags and EditorMarks.SPOILER != 0) {
82
+ paint.bgColor = 0x40595959
83
+ }
84
+ }
85
+
86
+ private fun apply(paint: TextPaint) {
87
+ var style = paint.typeface?.style ?: Typeface.NORMAL
88
+ if (flags and EditorMarks.BOLD != 0) {
89
+ style = style or Typeface.BOLD
90
+ }
91
+ if (flags and EditorMarks.ITALIC != 0) {
92
+ style = style or Typeface.ITALIC
93
+ }
94
+ val base =
95
+ if (flags and EditorMarks.INLINE_CODE != 0) Typeface.MONOSPACE else paint.typeface
96
+ paint.typeface = Typeface.create(base, style)
97
+
98
+ // Sup/sub match the viewer's 0.7 scaling.
99
+ if (flags and (EditorMarks.SUPERSCRIPT or EditorMarks.SUBSCRIPT) != 0) {
100
+ val size = paint.textSize
101
+ paint.textSize = size * 0.7f
102
+ if (flags and EditorMarks.SUPERSCRIPT != 0) {
103
+ paint.baselineShift -= (size * 0.33f).toInt()
104
+ } else {
105
+ paint.baselineShift += (size * 0.15f).toInt()
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ /** Heading lines scale up and embolden. */
112
+ class HeadingDisplaySpan(level: Int) : MetricAffectingSpan(), EditorDerivedSpan {
113
+ private val scale = when (level) {
114
+ 1 -> 2f
115
+ 2 -> 1.5f
116
+ 3 -> 1.25f
117
+ 4 -> 1.125f
118
+ 5 -> 1f
119
+ else -> 0.875f
120
+ }
121
+
122
+ override fun updateMeasureState(paint: TextPaint) = apply(paint)
123
+
124
+ override fun updateDrawState(paint: TextPaint) = apply(paint)
125
+
126
+ private fun apply(paint: TextPaint) {
127
+ paint.textSize = paint.textSize * scale
128
+ paint.isFakeBoldText = true
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Code lines: monospace glyphs. The full-width background stripe is drawn
134
+ * by the view (spans cannot cover empty lines).
135
+ */
136
+ class CodeLineDisplaySpan : MetricAffectingSpan(), EditorDerivedSpan {
137
+ override fun updateMeasureState(paint: TextPaint) {
138
+ paint.typeface = Typeface.MONOSPACE
139
+ }
140
+
141
+ override fun updateDrawState(paint: TextPaint) {
142
+ paint.typeface = Typeface.MONOSPACE
143
+ }
144
+ }
145
+
146
+ /** Quote lines: leading margin with a vertical bar. */
147
+ class QuoteDisplaySpan(private val density: Float) : LeadingMarginSpan, EditorDerivedSpan {
148
+ override fun getLeadingMargin(first: Boolean): Int = (16 * density).toInt()
149
+
150
+ override fun drawLeadingMargin(
151
+ canvas: Canvas,
152
+ paint: Paint,
153
+ x: Int,
154
+ dir: Int,
155
+ top: Int,
156
+ baseline: Int,
157
+ bottom: Int,
158
+ text: CharSequence,
159
+ start: Int,
160
+ end: Int,
161
+ first: Boolean,
162
+ layout: Layout,
163
+ ) {
164
+ val previous = paint.color
165
+ val width = 3 * density
166
+ paint.color = (paint.color and 0x00FFFFFF) or -0x67000000
167
+ val barLeft = x + dir * 4 * density
168
+ canvas.drawRect(barLeft, top.toFloat(), barLeft + dir * width, bottom.toFloat(), paint)
169
+ paint.color = previous
170
+ }
171
+ }
172
+
173
+ /** Line-height span the display refresh can remove wholesale. */
174
+ class EditorLineHeightSpan(heightPx: Int) :
175
+ com.jetmarkdown.render.spans.MarkdownLineHeightSpan(heightPx),
176
+ EditorDerivedSpan
177
+
178
+ /**
179
+ * No-op run splitter for a heading's newline: keeps it out of the
180
+ * heading's font run so the trailing empty line's caret doesn't inherit
181
+ * heading metrics.
182
+ */
183
+ class NewlineResetSpan : MetricAffectingSpan(), EditorDerivedSpan {
184
+ override fun updateMeasureState(paint: TextPaint) = Unit
185
+
186
+ override fun updateDrawState(paint: TextPaint) = Unit
187
+ }
188
+
189
+ /** List lines: leading margin drawing the bullet or the item number. */
190
+ class ListMarkerDisplaySpan(
191
+ private val marker: String,
192
+ private val density: Float,
193
+ ) : LeadingMarginSpan, EditorDerivedSpan {
194
+ override fun getLeadingMargin(first: Boolean): Int = (28 * density).toInt()
195
+
196
+ override fun drawLeadingMargin(
197
+ canvas: Canvas,
198
+ paint: Paint,
199
+ x: Int,
200
+ dir: Int,
201
+ top: Int,
202
+ baseline: Int,
203
+ bottom: Int,
204
+ text: CharSequence,
205
+ start: Int,
206
+ end: Int,
207
+ first: Boolean,
208
+ layout: Layout,
209
+ ) {
210
+ if (!first) {
211
+ return
212
+ }
213
+ val width = paint.measureText(marker)
214
+ val position = x + dir * ((24 * density) - width - (6 * density))
215
+ canvas.drawText(marker, position, baseline.toFloat(), paint)
216
+ }
217
+ }