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,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
|
+
}
|