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