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,54 @@
|
|
|
1
|
+
package com.jetmarkdown.measure
|
|
2
|
+
|
|
3
|
+
import android.content.res.Resources
|
|
4
|
+
import com.jetmarkdown.render.ContentCache
|
|
5
|
+
import com.jetmarkdown.style.StyleConfig
|
|
6
|
+
import org.json.JSONObject
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Called from the C++ shadow node (via JNI) on the Fabric layout thread.
|
|
10
|
+
* Receives dp, works in px, returns dp.
|
|
11
|
+
*/
|
|
12
|
+
object MarkdownMeasurer {
|
|
13
|
+
@JvmName("measure")
|
|
14
|
+
fun measure(
|
|
15
|
+
markdown: ByteArray,
|
|
16
|
+
stylesJson: ByteArray,
|
|
17
|
+
imagesJson: ByteArray,
|
|
18
|
+
maxWidth: Float,
|
|
19
|
+
fontScale: Float,
|
|
20
|
+
): Float {
|
|
21
|
+
val markdownString = String(markdown, Charsets.UTF_8)
|
|
22
|
+
val stylesString = String(stylesJson, Charsets.UTF_8)
|
|
23
|
+
val imagesString = String(imagesJson, Charsets.UTF_8)
|
|
24
|
+
|
|
25
|
+
val density = Resources.getSystem().displayMetrics.density
|
|
26
|
+
val styles = StyleConfig.from(stylesString)
|
|
27
|
+
val content = ContentCache.get(markdownString, stylesString, fontScale)
|
|
28
|
+
|
|
29
|
+
val horizontalPadding = styles.paddingLeft + styles.paddingRight
|
|
30
|
+
val contentWidthPx = ((maxWidth - horizontalPadding) * density).toInt()
|
|
31
|
+
if (contentWidthPx <= 0) {
|
|
32
|
+
return 0f
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
val layout = content.layoutFor(contentWidthPx, parseImageSizes(imagesString))
|
|
36
|
+
return layout.totalHeightPx / density
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** {"url":[w,h],...} in dp. */
|
|
40
|
+
fun parseImageSizes(json: String): Map<String, FloatArray> {
|
|
41
|
+
if (json.isEmpty() || json == "{}") {
|
|
42
|
+
return emptyMap()
|
|
43
|
+
}
|
|
44
|
+
val root = runCatching { JSONObject(json) }.getOrNull() ?: return emptyMap()
|
|
45
|
+
val sizes = HashMap<String, FloatArray>()
|
|
46
|
+
for (key in root.keys()) {
|
|
47
|
+
val pair = root.optJSONArray(key) ?: continue
|
|
48
|
+
if (pair.length() == 2) {
|
|
49
|
+
sizes[key] = floatArrayOf(pair.optDouble(0).toFloat(), pair.optDouble(1).toFloat())
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return sizes
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
package com.jetmarkdown.parser
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Decodes the flat binary AST produced by cpp/core/AstSerializer.cpp.
|
|
5
|
+
* Layout per node (little endian):
|
|
6
|
+
* u8 type, u8 level, u8 flags (bit 0 ordered), i32 startIndex,
|
|
7
|
+
* u32 textLen + bytes, u32 urlLen + bytes, u32 childCount, children...
|
|
8
|
+
*/
|
|
9
|
+
object AstDecoder {
|
|
10
|
+
fun decode(bytes: ByteArray): MdNode {
|
|
11
|
+
if (bytes.isEmpty()) {
|
|
12
|
+
return MdNode(MdNodeType.DOCUMENT, "", "", 0, false, 1, emptyList())
|
|
13
|
+
}
|
|
14
|
+
return Reader(bytes).readNode()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private class Reader(private val bytes: ByteArray) {
|
|
18
|
+
private var pos = 0
|
|
19
|
+
|
|
20
|
+
fun readNode(): MdNode {
|
|
21
|
+
val type = MdNodeType.from(readU8())
|
|
22
|
+
val level = readU8()
|
|
23
|
+
val flags = readU8()
|
|
24
|
+
val startIndex = readI32()
|
|
25
|
+
val text = readString()
|
|
26
|
+
val url = readString()
|
|
27
|
+
val childCount = readI32()
|
|
28
|
+
val children = if (childCount > 0) {
|
|
29
|
+
ArrayList<MdNode>(childCount).also { list ->
|
|
30
|
+
repeat(childCount) { list.add(readNode()) }
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
emptyList()
|
|
34
|
+
}
|
|
35
|
+
return MdNode(type, text, url, level, flags and 1 == 1, startIndex, children)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private fun readU8(): Int = bytes[pos++].toInt() and 0xFF
|
|
39
|
+
|
|
40
|
+
private fun readI32(): Int {
|
|
41
|
+
val value = (bytes[pos].toInt() and 0xFF) or
|
|
42
|
+
((bytes[pos + 1].toInt() and 0xFF) shl 8) or
|
|
43
|
+
((bytes[pos + 2].toInt() and 0xFF) shl 16) or
|
|
44
|
+
((bytes[pos + 3].toInt() and 0xFF) shl 24)
|
|
45
|
+
pos += 4
|
|
46
|
+
return value
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private fun readString(): String {
|
|
50
|
+
val length = readI32()
|
|
51
|
+
if (length == 0) {
|
|
52
|
+
return ""
|
|
53
|
+
}
|
|
54
|
+
val value = String(bytes, pos, length, Charsets.UTF_8)
|
|
55
|
+
pos += length
|
|
56
|
+
return value
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package com.jetmarkdown.parser
|
|
2
|
+
|
|
3
|
+
enum class MdNodeType(val id: Int) {
|
|
4
|
+
DOCUMENT(0),
|
|
5
|
+
HEADING(1),
|
|
6
|
+
PARAGRAPH(2),
|
|
7
|
+
BLOCK_QUOTE(3),
|
|
8
|
+
CODE_BLOCK(4),
|
|
9
|
+
LIST(5),
|
|
10
|
+
LIST_ITEM(6),
|
|
11
|
+
TABLE(7),
|
|
12
|
+
TABLE_ROW(8),
|
|
13
|
+
TABLE_CELL(9),
|
|
14
|
+
IMAGE(10),
|
|
15
|
+
THEMATIC_BREAK(11),
|
|
16
|
+
TEXT(12),
|
|
17
|
+
SOFT_BREAK(13),
|
|
18
|
+
HARD_BREAK(14),
|
|
19
|
+
BOLD(15),
|
|
20
|
+
ITALIC(16),
|
|
21
|
+
STRIKETHROUGH(17),
|
|
22
|
+
LINK(18),
|
|
23
|
+
INLINE_CODE(19),
|
|
24
|
+
SPOILER(20),
|
|
25
|
+
SUPERSCRIPT(21),
|
|
26
|
+
SUBSCRIPT(22);
|
|
27
|
+
|
|
28
|
+
companion object {
|
|
29
|
+
private val byId = entries.associateBy { it.id }
|
|
30
|
+
|
|
31
|
+
fun from(id: Int): MdNodeType = byId[id] ?: TEXT
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class MdNode(
|
|
36
|
+
val type: MdNodeType,
|
|
37
|
+
val text: String,
|
|
38
|
+
val url: String,
|
|
39
|
+
val level: Int,
|
|
40
|
+
val ordered: Boolean,
|
|
41
|
+
val startIndex: Int,
|
|
42
|
+
val children: List<MdNode>,
|
|
43
|
+
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
package com.jetmarkdown.render
|
|
2
|
+
|
|
3
|
+
import android.content.res.Resources
|
|
4
|
+
import android.util.LruCache
|
|
5
|
+
import com.jetmarkdown.JetMarkdownNative
|
|
6
|
+
import com.jetmarkdown.style.StyleConfig
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Shared parse/render cache. The Fabric measurer populates it on the layout
|
|
10
|
+
* thread; the mounted view reads the same entry on the main thread, so a
|
|
11
|
+
* mount never re-parses.
|
|
12
|
+
*/
|
|
13
|
+
object ContentCache {
|
|
14
|
+
private data class Key(
|
|
15
|
+
val markdown: String,
|
|
16
|
+
val stylesJson: String,
|
|
17
|
+
val fontScale: Float,
|
|
18
|
+
// Rendered spans hold theme-resolved platform colors; a dark-mode flip
|
|
19
|
+
// must not serve light-resolved content.
|
|
20
|
+
val appearance: Int,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
private val cache = object : LruCache<Key, RenderedContent>(64) {}
|
|
24
|
+
|
|
25
|
+
fun get(markdown: String, stylesJson: String, fontScale: Float): RenderedContent {
|
|
26
|
+
val key = Key(
|
|
27
|
+
markdown,
|
|
28
|
+
stylesJson,
|
|
29
|
+
fontScale,
|
|
30
|
+
com.jetmarkdown.style.PlatformColorResolver.appearanceKey(),
|
|
31
|
+
)
|
|
32
|
+
synchronized(cache) {
|
|
33
|
+
cache.get(key)?.let { return it }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
val density = Resources.getSystem().displayMetrics.density
|
|
37
|
+
val styles = StyleConfig.from(stylesJson)
|
|
38
|
+
val root = JetMarkdownNative.parseMarkdown(markdown)
|
|
39
|
+
val blocks = SpannableRenderer.render(root, styles, density, fontScale)
|
|
40
|
+
val content = RenderedContent(
|
|
41
|
+
blocks = blocks,
|
|
42
|
+
gapPx = styles.gap * density,
|
|
43
|
+
topPaddingPx = styles.paddingTop * density,
|
|
44
|
+
bottomPaddingPx = styles.paddingBottom * density,
|
|
45
|
+
density = density,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
synchronized(cache) {
|
|
49
|
+
cache.put(key, content)
|
|
50
|
+
}
|
|
51
|
+
return content
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
package com.jetmarkdown.render
|
|
2
|
+
|
|
3
|
+
import android.text.Layout
|
|
4
|
+
import android.text.StaticLayout
|
|
5
|
+
import android.text.TextPaint
|
|
6
|
+
import com.jetmarkdown.style.LayoutStyleSpec
|
|
7
|
+
|
|
8
|
+
/** One renderable block; blocks nest (quote children, list row content). */
|
|
9
|
+
sealed class Block {
|
|
10
|
+
class Text(
|
|
11
|
+
val text: CharSequence,
|
|
12
|
+
val paint: TextPaint,
|
|
13
|
+
val spoilerColor: Int = 0xFF3F3F46.toInt(),
|
|
14
|
+
val spoilerRadiusPx: Float = 0f,
|
|
15
|
+
) : Block()
|
|
16
|
+
|
|
17
|
+
/** Code renders unwrapped inside a horizontal scroller. */
|
|
18
|
+
class Code(val text: CharSequence, val paint: TextPaint, val style: LayoutStyleSpec) : Block()
|
|
19
|
+
|
|
20
|
+
class Quote(val children: List<Block>, val style: LayoutStyleSpec) : Block()
|
|
21
|
+
|
|
22
|
+
class ListRow(val marker: CharSequence, val markerPaint: TextPaint, val content: List<Block>)
|
|
23
|
+
|
|
24
|
+
class ListBlock(
|
|
25
|
+
val rows: List<ListRow>,
|
|
26
|
+
val marginLeftPx: Float,
|
|
27
|
+
val markerWidthPx: Float,
|
|
28
|
+
val markerMarginLeftPx: Float,
|
|
29
|
+
) : Block()
|
|
30
|
+
|
|
31
|
+
class Divider(val color: Int, val thicknessPx: Float) : Block()
|
|
32
|
+
|
|
33
|
+
class Image(
|
|
34
|
+
val url: String,
|
|
35
|
+
val backgroundColor: Int?,
|
|
36
|
+
val borderRadiusPx: Float,
|
|
37
|
+
val heightPx: Float,
|
|
38
|
+
val maxHeightPx: Float,
|
|
39
|
+
val placeholderPx: Float,
|
|
40
|
+
) : Block()
|
|
41
|
+
|
|
42
|
+
class TableRowData(val isHeader: Boolean, val cells: List<CharSequence>)
|
|
43
|
+
|
|
44
|
+
class Table(
|
|
45
|
+
val rows: List<TableRowData>,
|
|
46
|
+
val cellPaint: TextPaint,
|
|
47
|
+
val style: LayoutStyleSpec,
|
|
48
|
+
val headerRowStyle: LayoutStyleSpec,
|
|
49
|
+
val bodyRowStyle: LayoutStyleSpec,
|
|
50
|
+
val cellPaddingLeftPx: Float,
|
|
51
|
+
val cellPaddingRightPx: Float,
|
|
52
|
+
val cellPaddingTopPx: Float,
|
|
53
|
+
val cellPaddingBottomPx: Float,
|
|
54
|
+
val headerCellPaddingLeftPx: Float,
|
|
55
|
+
val headerCellPaddingRightPx: Float,
|
|
56
|
+
val headerCellPaddingTopPx: Float,
|
|
57
|
+
val headerCellPaddingBottomPx: Float,
|
|
58
|
+
val minColumnWidthPx: Float,
|
|
59
|
+
val maxColumnWidthPx: Float,
|
|
60
|
+
) : Block() {
|
|
61
|
+
fun rowStyle(isHeader: Boolean): LayoutStyleSpec =
|
|
62
|
+
if (isHeader) headerRowStyle else bodyRowStyle
|
|
63
|
+
|
|
64
|
+
fun padLeft(isHeader: Boolean): Float =
|
|
65
|
+
if (isHeader) headerCellPaddingLeftPx else cellPaddingLeftPx
|
|
66
|
+
|
|
67
|
+
fun padRight(isHeader: Boolean): Float =
|
|
68
|
+
if (isHeader) headerCellPaddingRightPx else cellPaddingRightPx
|
|
69
|
+
|
|
70
|
+
fun padTop(isHeader: Boolean): Float =
|
|
71
|
+
if (isHeader) headerCellPaddingTopPx else cellPaddingTopPx
|
|
72
|
+
|
|
73
|
+
fun padBottom(isHeader: Boolean): Float =
|
|
74
|
+
if (isHeader) headerCellPaddingBottomPx else cellPaddingBottomPx
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Layout results for one block at one width. */
|
|
79
|
+
class MeasuredBlock(
|
|
80
|
+
val block: Block,
|
|
81
|
+
val heightPx: Float,
|
|
82
|
+
val textLayout: StaticLayout?,
|
|
83
|
+
/** Code/Table: unwrapped content width for the scroller. */
|
|
84
|
+
val contentWidthPx: Float,
|
|
85
|
+
val children: List<MeasuredBlock>,
|
|
86
|
+
/** List rows: marker layouts parallel to children groups. */
|
|
87
|
+
val markerLayouts: List<StaticLayout>,
|
|
88
|
+
/** List rows: measured content per row. */
|
|
89
|
+
val rowContents: List<List<MeasuredBlock>>,
|
|
90
|
+
/** Tables: resolved column widths. */
|
|
91
|
+
val columnWidths: FloatArray = FloatArray(0),
|
|
92
|
+
/** Tables: per-row heights. */
|
|
93
|
+
val rowHeights: FloatArray = FloatArray(0),
|
|
94
|
+
/** Tables: cell layouts, row-major. */
|
|
95
|
+
val cellLayouts: List<List<StaticLayout>> = emptyList(),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Parsed + rendered markdown, shared between the Fabric measurer (layout
|
|
100
|
+
* thread) and the mounted view (main thread). Per-width results are cached.
|
|
101
|
+
*/
|
|
102
|
+
class RenderedContent(
|
|
103
|
+
val blocks: List<Block>,
|
|
104
|
+
private val gapPx: Float,
|
|
105
|
+
private val topPaddingPx: Float,
|
|
106
|
+
private val bottomPaddingPx: Float,
|
|
107
|
+
private val density: Float,
|
|
108
|
+
) {
|
|
109
|
+
class WidthLayout(val measured: List<MeasuredBlock>, val totalHeightPx: Float)
|
|
110
|
+
|
|
111
|
+
private data class LayoutKey(val widthPx: Int, val sizesHash: Int)
|
|
112
|
+
|
|
113
|
+
private val layoutCache = HashMap<LayoutKey, WidthLayout>()
|
|
114
|
+
|
|
115
|
+
/** imageSizes: url -> intrinsic dp size, merged prop + discovered. */
|
|
116
|
+
@Synchronized
|
|
117
|
+
fun layoutFor(widthPx: Int, imageSizes: Map<String, FloatArray> = emptyMap()): WidthLayout {
|
|
118
|
+
val key = LayoutKey(widthPx, imageSizesHash(imageSizes))
|
|
119
|
+
layoutCache[key]?.let { return it }
|
|
120
|
+
|
|
121
|
+
val measured = blocks.map { measure(it, widthPx.toFloat(), imageSizes) }
|
|
122
|
+
var height = topPaddingPx + bottomPaddingPx
|
|
123
|
+
measured.forEachIndexed { index, block ->
|
|
124
|
+
height += block.heightPx
|
|
125
|
+
if (index < measured.size - 1) {
|
|
126
|
+
height += gapPx
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
val result = WidthLayout(measured, height)
|
|
131
|
+
if (layoutCache.size > 4) {
|
|
132
|
+
layoutCache.clear()
|
|
133
|
+
}
|
|
134
|
+
layoutCache[key] = result
|
|
135
|
+
return result
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private fun imageSizesHash(sizes: Map<String, FloatArray>): Int {
|
|
139
|
+
var hash = sizes.size
|
|
140
|
+
for ((url, size) in sizes) {
|
|
141
|
+
hash = hash * 31 + url.hashCode()
|
|
142
|
+
hash = hash * 31 + size[0].toInt()
|
|
143
|
+
hash = hash * 31 + size[1].toInt()
|
|
144
|
+
}
|
|
145
|
+
return hash
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fun stackHeight(children: List<MeasuredBlock>): Float {
|
|
149
|
+
var height = 0f
|
|
150
|
+
children.forEachIndexed { index, child ->
|
|
151
|
+
height += child.heightPx
|
|
152
|
+
if (index < children.size - 1) {
|
|
153
|
+
height += gapPx
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return height
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
val gap: Float get() = gapPx
|
|
160
|
+
|
|
161
|
+
private fun measure(
|
|
162
|
+
block: Block,
|
|
163
|
+
widthPx: Float,
|
|
164
|
+
imageSizes: Map<String, FloatArray>,
|
|
165
|
+
): MeasuredBlock {
|
|
166
|
+
return when (block) {
|
|
167
|
+
is Block.Text -> {
|
|
168
|
+
val layout = staticLayout(block.text, block.paint, widthPx.toInt(), wrap = true)
|
|
169
|
+
MeasuredBlock(block, layout.height.toFloat(), layout, widthPx, emptyList(), emptyList(), emptyList())
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
is Block.Code -> {
|
|
173
|
+
val desired = Layout.getDesiredWidth(block.text, block.paint)
|
|
174
|
+
val layout = staticLayout(block.text, block.paint, kotlin.math.ceil(desired.toDouble()).toInt(), wrap = false)
|
|
175
|
+
val height = layout.height + block.style.paddingTop + block.style.paddingBottom
|
|
176
|
+
MeasuredBlock(block, height, layout, desired, emptyList(), emptyList(), emptyList())
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
is Block.Quote -> {
|
|
180
|
+
val innerWidth = widthPx - block.style.paddingLeft - block.style.paddingRight -
|
|
181
|
+
block.style.borderLeftWidth - block.style.borderRightWidth
|
|
182
|
+
val children = block.children.map { measure(it, innerWidth.coerceAtLeast(1f), imageSizes) }
|
|
183
|
+
val height = stackHeight(children) + block.style.paddingTop + block.style.paddingBottom +
|
|
184
|
+
block.style.borderTopWidth + block.style.borderBottomWidth
|
|
185
|
+
MeasuredBlock(block, height, null, widthPx, children, emptyList(), emptyList())
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
is Block.ListBlock -> {
|
|
189
|
+
val contentX = block.marginLeftPx + block.markerMarginLeftPx + block.markerWidthPx
|
|
190
|
+
val contentWidth = (widthPx - contentX).coerceAtLeast(1f)
|
|
191
|
+
val markerLayouts = ArrayList<StaticLayout>(block.rows.size)
|
|
192
|
+
val rowContents = ArrayList<List<MeasuredBlock>>(block.rows.size)
|
|
193
|
+
var height = 0f
|
|
194
|
+
block.rows.forEachIndexed { index, row ->
|
|
195
|
+
val marker = staticLayout(row.marker, row.markerPaint, block.markerWidthPx.toInt().coerceAtLeast(1), wrap = true)
|
|
196
|
+
val content = row.content.map { measure(it, contentWidth, imageSizes) }
|
|
197
|
+
markerLayouts.add(marker)
|
|
198
|
+
rowContents.add(content)
|
|
199
|
+
height += maxOf(marker.height.toFloat(), stackHeight(content))
|
|
200
|
+
if (index < block.rows.size - 1) {
|
|
201
|
+
height += gapPx / 2f
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
MeasuredBlock(block, height, null, contentWidth, emptyList(), markerLayouts, rowContents)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
is Block.Divider ->
|
|
208
|
+
MeasuredBlock(block, block.thicknessPx, null, widthPx, emptyList(), emptyList(), emptyList())
|
|
209
|
+
|
|
210
|
+
is Block.Table -> measureTable(block, widthPx)
|
|
211
|
+
|
|
212
|
+
is Block.Image -> {
|
|
213
|
+
val known = imageSizes[block.url]
|
|
214
|
+
var displayH: Float
|
|
215
|
+
var displayW: Float
|
|
216
|
+
if (known != null && known[0] > 0 && known[1] > 0) {
|
|
217
|
+
val intrinsicW = known[0] * density
|
|
218
|
+
val intrinsicH = known[1] * density
|
|
219
|
+
val scale = (widthPx / intrinsicW).coerceAtMost(1f)
|
|
220
|
+
displayH = intrinsicH * scale
|
|
221
|
+
if (block.heightPx > 0) {
|
|
222
|
+
displayH = block.heightPx
|
|
223
|
+
}
|
|
224
|
+
if (block.maxHeightPx > 0) {
|
|
225
|
+
displayH = displayH.coerceAtMost(block.maxHeightPx)
|
|
226
|
+
}
|
|
227
|
+
displayW = (intrinsicW * displayH / intrinsicH).coerceAtMost(widthPx)
|
|
228
|
+
} else {
|
|
229
|
+
// Full-width placeholder until the intrinsic size is known.
|
|
230
|
+
displayH = if (block.heightPx > 0) block.heightPx else block.placeholderPx
|
|
231
|
+
if (block.maxHeightPx > 0) {
|
|
232
|
+
displayH = displayH.coerceAtMost(block.maxHeightPx)
|
|
233
|
+
}
|
|
234
|
+
displayW = widthPx
|
|
235
|
+
}
|
|
236
|
+
MeasuredBlock(block, displayH, null, displayW, emptyList(), emptyList(), emptyList())
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Intelligent column widths: natural (unwrapped) width per column,
|
|
242
|
+
// clamped to [min, max]; surplus distributed proportionally when the
|
|
243
|
+
// table fits, horizontal scroll when it does not.
|
|
244
|
+
private fun measureTable(block: Block.Table, widthPx: Float): MeasuredBlock {
|
|
245
|
+
val columnCount = block.rows.maxOfOrNull { it.cells.size } ?: 0
|
|
246
|
+
if (columnCount == 0) {
|
|
247
|
+
return MeasuredBlock(block, 0f, null, widthPx, emptyList(), emptyList(), emptyList())
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
val natural = FloatArray(columnCount)
|
|
251
|
+
for (row in block.rows) {
|
|
252
|
+
val cellPadH = block.padLeft(row.isHeader) + block.padRight(row.isHeader)
|
|
253
|
+
row.cells.forEachIndexed { column, cell ->
|
|
254
|
+
// Ceil so the later int truncation of the cell width can't force
|
|
255
|
+
// a wrap that the natural measurement said would fit.
|
|
256
|
+
val desired =
|
|
257
|
+
kotlin.math.ceil(Layout.getDesiredWidth(cell, block.cellPaint).toDouble()).toFloat() +
|
|
258
|
+
cellPadH + 1f
|
|
259
|
+
if (desired > natural[column]) {
|
|
260
|
+
natural[column] = desired
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
val columnWidths = FloatArray(columnCount) {
|
|
266
|
+
natural[it].coerceIn(
|
|
267
|
+
block.minColumnWidthPx,
|
|
268
|
+
if (block.maxColumnWidthPx > 0) block.maxColumnWidthPx else Float.MAX_VALUE,
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
val availableWidth = widthPx - block.style.paddingLeft - block.style.paddingRight -
|
|
273
|
+
block.style.borderLeftWidth - block.style.borderRightWidth
|
|
274
|
+
val total = columnWidths.sum()
|
|
275
|
+
if (total < availableWidth) {
|
|
276
|
+
val naturalTotal = natural.sum().coerceAtLeast(1f)
|
|
277
|
+
val surplus = availableWidth - total
|
|
278
|
+
for (i in 0 until columnCount) {
|
|
279
|
+
columnWidths[i] += surplus * (natural[i] / naturalTotal)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
val contentWidth = columnWidths.sum()
|
|
283
|
+
|
|
284
|
+
val rowHeights = FloatArray(block.rows.size)
|
|
285
|
+
val cellLayouts = block.rows.mapIndexed { rowIndex, row ->
|
|
286
|
+
val rowStyle = block.rowStyle(row.isHeader)
|
|
287
|
+
val cellPadH = block.padLeft(row.isHeader) + block.padRight(row.isHeader)
|
|
288
|
+
val cellPadV = block.padTop(row.isHeader) + block.padBottom(row.isHeader)
|
|
289
|
+
val rowExtra = rowStyle.borderBottomWidth + rowStyle.borderTopWidth
|
|
290
|
+
val layouts = row.cells.mapIndexed { column, cell ->
|
|
291
|
+
staticLayout(
|
|
292
|
+
cell,
|
|
293
|
+
block.cellPaint,
|
|
294
|
+
(columnWidths[column] - cellPadH).toInt().coerceAtLeast(1),
|
|
295
|
+
wrap = true,
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
rowHeights[rowIndex] =
|
|
299
|
+
(layouts.maxOfOrNull { it.height.toFloat() } ?: 0f) + cellPadV + rowExtra
|
|
300
|
+
layouts
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
val height = rowHeights.sum() + block.style.paddingTop + block.style.paddingBottom +
|
|
304
|
+
block.style.borderTopWidth + block.style.borderBottomWidth
|
|
305
|
+
return MeasuredBlock(
|
|
306
|
+
block, height, null, contentWidth, emptyList(), emptyList(), emptyList(),
|
|
307
|
+
columnWidths, rowHeights, cellLayouts,
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private fun staticLayout(text: CharSequence, paint: TextPaint, widthPx: Int, wrap: Boolean): StaticLayout {
|
|
312
|
+
return StaticLayout.Builder
|
|
313
|
+
.obtain(text, 0, text.length, paint, widthPx.coerceAtLeast(1))
|
|
314
|
+
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
|
|
315
|
+
.setIncludePad(false)
|
|
316
|
+
.build()
|
|
317
|
+
}
|
|
318
|
+
}
|