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,658 @@
1
+ package com.jetmarkdown.render
2
+
3
+ import android.graphics.Color
4
+ import android.graphics.Typeface
5
+ import android.os.Build
6
+ import android.text.Layout
7
+ import android.text.SpannableStringBuilder
8
+ import android.text.Spanned
9
+ import android.text.TextPaint
10
+ import com.jetmarkdown.parser.MdNode
11
+ import com.jetmarkdown.parser.MdNodeType
12
+ import com.jetmarkdown.render.spans.ChipSpan
13
+ import com.jetmarkdown.render.spans.LinkSpan
14
+ import com.jetmarkdown.render.spans.MarkdownLineHeightSpan
15
+ import com.jetmarkdown.render.spans.RunSpan
16
+ import com.jetmarkdown.render.spans.SpoilerSpan
17
+ import com.jetmarkdown.style.Fonts
18
+ import com.jetmarkdown.style.LayoutStyleSpec
19
+ import com.jetmarkdown.style.PlatformColorResolver
20
+ import com.jetmarkdown.style.StyleConfig
21
+ import com.jetmarkdown.style.TextStyleSpec
22
+ import kotlin.math.ceil
23
+ import org.json.JSONObject
24
+
25
+ /**
26
+ * AST -> renderable block tree. Inline runs carry one RunSpan each with the
27
+ * fully-resolved attributes (mirrors the iOS attribute-stack renderer).
28
+ */
29
+ object SpannableRenderer {
30
+
31
+ // Unstyled output is fully plain: no backgrounds, borders, or paddings
32
+ // unless the styles prop (e.g. defaultStyles on the JS side) provides
33
+ // them.
34
+ private val PLAIN_LAYOUT = LayoutStyleSpec(
35
+ backgroundColor = null,
36
+ paddingLeft = 0f, paddingRight = 0f, paddingTop = 0f, paddingBottom = 0f,
37
+ borderRadius = 0f,
38
+ borderLeftColor = null, borderLeftWidth = 0f,
39
+ borderRightColor = null, borderRightWidth = 0f,
40
+ borderTopColor = null, borderTopWidth = 0f,
41
+ borderBottomColor = null, borderBottomWidth = 0f,
42
+ )
43
+
44
+ /** Fully-resolved text attributes at one point of the inline walk. */
45
+ private data class ResolvedAttrs(
46
+ val fontSizePx: Float,
47
+ val lineHeightPx: Int = 0, // 0 = natural
48
+ val weight: Int = 400,
49
+ val italic: Boolean = false,
50
+ val family: String? = null,
51
+ val color: Int = Color.BLACK,
52
+ val variants: List<String>? = null,
53
+ val underline: Boolean = false,
54
+ val strikethrough: Boolean = false,
55
+ val baselineShiftPx: Int = 0,
56
+ val backgroundColor: Int? = null,
57
+ // Chip geometry for drawn run backgrounds (inlineCode/link/mention).
58
+ // A link/mention node attaches ONE ChipSpan over its whole range so a
59
+ // mixed-styling link doesn't fragment into seamed per-run chips.
60
+ val suppressRunChip: Boolean = false,
61
+ val chipRadiusPx: Float = 0f,
62
+ val chipPadLeftPx: Float = 0f,
63
+ val chipPadRightPx: Float = 0f,
64
+ val linkUrl: String? = null,
65
+ val spoilerId: Int? = null,
66
+ )
67
+
68
+ private class Context(
69
+ val styles: StyleConfig,
70
+ val density: Float,
71
+ val fontScale: Float,
72
+ ) {
73
+ private var spoilerCounter = 0
74
+
75
+ fun nextSpoilerId(): Int = spoilerCounter++
76
+
77
+ fun apply(attrs: ResolvedAttrs, spec: TextStyleSpec?): ResolvedAttrs {
78
+ if (spec == null) {
79
+ return attrs
80
+ }
81
+ var next = attrs
82
+ spec.fontSize?.let { next = next.copy(fontSizePx = it * density * fontScale) }
83
+ spec.lineHeight?.let { next = next.copy(lineHeightPx = (it * density * fontScale).toInt()) }
84
+ spec.fontWeight?.let { next = next.copy(weight = it) }
85
+ spec.fontFamily?.let { next = next.copy(family = it) }
86
+ spec.color?.let { next = next.copy(color = it) }
87
+ spec.fontVariant?.let { next = next.copy(variants = it) }
88
+ spec.textDecorationLine?.let {
89
+ next = next.copy(
90
+ underline = it.contains("underline"),
91
+ strikethrough = it.contains("line-through"),
92
+ )
93
+ }
94
+ spec.backgroundColor?.let { next = next.copy(backgroundColor = it) }
95
+ return next
96
+ }
97
+
98
+ fun apply(attrs: ResolvedAttrs, key: String): ResolvedAttrs =
99
+ apply(attrs, styles.textStyleFor(key))
100
+
101
+ fun layoutStyle(key: String, defaults: LayoutStyleSpec): LayoutStyleSpec {
102
+ val spec = LayoutStyleSpec.from(styles.rawSection(key), defaults)
103
+ return spec.scaled(density)
104
+ }
105
+ }
106
+
107
+ fun render(root: MdNode, styles: StyleConfig, density: Float, fontScale: Float): List<Block> {
108
+ val context = Context(styles, density, fontScale)
109
+ return renderBlocks(root.children, context, inherited = null)
110
+ }
111
+
112
+ // Coalesces stray inline children (tight list items) into paragraphs and
113
+ // renders each block child.
114
+ private fun renderBlocks(
115
+ children: List<MdNode>,
116
+ context: Context,
117
+ inherited: TextStyleSpec?,
118
+ ): List<Block> {
119
+ val out = ArrayList<Block>()
120
+ var inlineRun = ArrayList<MdNode>()
121
+
122
+ fun flushInline() {
123
+ if (inlineRun.isNotEmpty()) {
124
+ val synthetic = MdNode(MdNodeType.PARAGRAPH, "", "", 0, false, 1, inlineRun)
125
+ out.add(textBlock(synthetic, context, inherited))
126
+ inlineRun = ArrayList()
127
+ }
128
+ }
129
+
130
+ for (child in children) {
131
+ if (isInline(child.type)) {
132
+ inlineRun.add(child)
133
+ } else {
134
+ flushInline()
135
+ renderBlock(child, context, inherited, out)
136
+ }
137
+ }
138
+ flushInline()
139
+ return out
140
+ }
141
+
142
+ private fun isInline(type: MdNodeType): Boolean = when (type) {
143
+ MdNodeType.TEXT, MdNodeType.SOFT_BREAK, MdNodeType.HARD_BREAK, MdNodeType.BOLD,
144
+ MdNodeType.ITALIC, MdNodeType.STRIKETHROUGH, MdNodeType.LINK, MdNodeType.INLINE_CODE,
145
+ MdNodeType.SPOILER, MdNodeType.SUPERSCRIPT, MdNodeType.SUBSCRIPT, MdNodeType.IMAGE,
146
+ -> true
147
+ else -> false
148
+ }
149
+
150
+ private fun renderBlock(
151
+ node: MdNode,
152
+ context: Context,
153
+ inherited: TextStyleSpec?,
154
+ out: MutableList<Block>,
155
+ ) {
156
+ when (node.type) {
157
+ MdNodeType.PARAGRAPH, MdNodeType.HEADING -> {
158
+ val image = singleImageChild(node)
159
+ if (image != null) {
160
+ out.add(imageBlock(image, context))
161
+ } else {
162
+ out.add(textBlock(node, context, inherited))
163
+ }
164
+ }
165
+
166
+ MdNodeType.BLOCK_QUOTE -> {
167
+ val layout = context.layoutStyle("blockQuote", PLAIN_LAYOUT)
168
+ val quoteText = merge(inherited, textStyleWithoutBackground(context.styles, "blockQuote"))
169
+ out.add(Block.Quote(renderBlocks(node.children, context, quoteText), layout))
170
+ }
171
+
172
+ MdNodeType.CODE_BLOCK -> {
173
+ val layout = context.layoutStyle("codeBlock", PLAIN_LAYOUT)
174
+ // Base cascades into code; the monospace family is semantic and
175
+ // only styles.codeBlock overrides it.
176
+ var attrs = context.apply(
177
+ ResolvedAttrs(fontSizePx = 16f * context.density * context.fontScale),
178
+ "base",
179
+ ).copy(family = "monospace")
180
+ attrs = context.apply(attrs, inherited)
181
+ attrs = context.apply(attrs, textStyleWithoutBackground(context.styles, "codeBlock"))
182
+ val text = SpannableStringBuilder()
183
+ appendRun(text, node.text.trimEnd('\n'), attrs)
184
+ out.add(Block.Code(text, basePaint(attrs), layout))
185
+ }
186
+
187
+ MdNodeType.LIST -> out.add(listBlock(node, context, inherited))
188
+
189
+ MdNodeType.TABLE -> out.add(tableBlock(node, context, inherited))
190
+
191
+ MdNodeType.THEMATIC_BREAK -> {
192
+ val section = context.styles.rawSection("divider")
193
+ // Neutral functional floor — a divider is content, so it stays
194
+ // visible even unstyled; defaultStyles provides the subtle hairline.
195
+ val color = section?.let { TextStyleSpec.from(it)?.color } ?: Color.BLACK
196
+ val height = section?.optDpOr("height", 1f) ?: 1f
197
+ out.add(Block.Divider(color, height * context.density))
198
+ }
199
+
200
+ else -> {
201
+ if (node.children.isNotEmpty()) {
202
+ out.addAll(renderBlocks(node.children, context, inherited))
203
+ } else if (node.text.isNotEmpty()) {
204
+ val synthetic = MdNode(
205
+ MdNodeType.PARAGRAPH, "", "", 0, false, 1,
206
+ listOf(MdNode(MdNodeType.TEXT, node.text, "", 0, false, 1, emptyList())),
207
+ )
208
+ out.add(textBlock(synthetic, context, inherited))
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ // A paragraph whose only meaningful child is one image renders as an
215
+ // image block (markdown images are inline; block display matches usage).
216
+ private fun singleImageChild(node: MdNode): MdNode? {
217
+ if (node.type != MdNodeType.PARAGRAPH) {
218
+ return null
219
+ }
220
+ var image: MdNode? = null
221
+ for (child in node.children) {
222
+ when (child.type) {
223
+ MdNodeType.IMAGE -> {
224
+ if (image != null) {
225
+ return null
226
+ }
227
+ image = child
228
+ }
229
+ MdNodeType.TEXT -> if (child.text.isNotBlank()) return null
230
+ MdNodeType.SOFT_BREAK, MdNodeType.HARD_BREAK -> Unit
231
+ else -> return null
232
+ }
233
+ }
234
+ return image
235
+ }
236
+
237
+ private fun imageBlock(node: MdNode, context: Context): Block.Image {
238
+ val section = context.styles.rawSection("image")
239
+ val density = context.density
240
+ return Block.Image(
241
+ url = node.url,
242
+ backgroundColor = section?.let { TextStyleSpec.from(it)?.backgroundColor },
243
+ borderRadiusPx = (section?.optDpOr("borderRadius", 0f) ?: 0f) * density,
244
+ heightPx = (section?.optDpOr("height", 0f) ?: 0f) * density,
245
+ maxHeightPx = (section?.optDpOr("maxHeight", 0f) ?: 0f) * density,
246
+ placeholderPx = 200f * density,
247
+ )
248
+ }
249
+
250
+ private fun listBlock(node: MdNode, context: Context, inherited: TextStyleSpec?): Block.ListBlock {
251
+ val styles = context.styles
252
+ val listSection = styles.rawSection("list")
253
+ val markerSection = styles.rawSection("listMarker")
254
+
255
+ val marginLeft = (listSection?.optDpOr("marginLeft", 0f) ?: 0f) * context.density
256
+ val markerMarginLeft = (markerSection?.optDpOr("marginLeft", 0f) ?: 0f) * context.density
257
+ val markerColor = markerSection?.let { TextStyleSpec.from(it)?.color }
258
+
259
+ val itemText = merge(inherited, styles.textStyleFor("listItem"))
260
+
261
+ var markerAttrs = ResolvedAttrs(fontSizePx = 16f * context.density * context.fontScale)
262
+ markerAttrs = context.apply(markerAttrs, "base")
263
+ markerAttrs = context.apply(markerAttrs, "paragraph")
264
+ markerAttrs = context.apply(markerAttrs, itemText)
265
+ if (markerColor != null) {
266
+ markerAttrs = markerAttrs.copy(color = markerColor)
267
+ }
268
+
269
+ val rows = ArrayList<Block.ListRow>()
270
+ var index = node.startIndex
271
+ var naturalMarkerWidth = 0f
272
+ for (item in node.children) {
273
+ if (item.type != MdNodeType.LIST_ITEM) {
274
+ continue
275
+ }
276
+ val markerText = if (node.ordered) "$index." else "•"
277
+ val marker = SpannableStringBuilder()
278
+ appendRun(marker, markerText, markerAttrs)
279
+ val markerPaint = basePaint(markerAttrs)
280
+ naturalMarkerWidth =
281
+ maxOf(naturalMarkerWidth, ceil(Layout.getDesiredWidth(marker, markerPaint)))
282
+ rows.add(
283
+ Block.ListRow(
284
+ marker = marker,
285
+ markerPaint = markerPaint,
286
+ content = renderBlocks(item.children, context, itemText),
287
+ )
288
+ )
289
+ index++
290
+ }
291
+ // Unstyled marker column is content-driven (widest marker); defaultStyles
292
+ // provides the classic fixed width.
293
+ val styledWidth = markerSection?.optDpOr("width", -1f) ?: -1f
294
+ val markerWidth = if (styledWidth >= 0f) styledWidth * context.density else naturalMarkerWidth
295
+ return Block.ListBlock(rows, marginLeft, markerWidth, markerMarginLeft)
296
+ }
297
+
298
+ private fun tableBlock(node: MdNode, context: Context, inherited: TextStyleSpec?): Block.Table {
299
+ val styles = context.styles
300
+ val density = context.density
301
+ val tableSection = styles.rawSection("table")
302
+ val cellSection = styles.rawSection("tableCell")
303
+
304
+ val headerCellSection = styles.rawSection("tableHeaderCell")
305
+
306
+ val cellText = merge(inherited, styles.textStyleFor("tableCell"))
307
+ var cellAttrs = ResolvedAttrs(fontSizePx = 16f * density * context.fontScale)
308
+ cellAttrs = context.apply(cellAttrs, "base")
309
+ cellAttrs = context.apply(cellAttrs, "paragraph")
310
+ cellAttrs = context.apply(cellAttrs, cellText)
311
+
312
+ val cellPadding = cellSection.optPadding(density, defaultAll = 0f)
313
+ // Header cells fall back to the body cell padding key-by-key.
314
+ val headerCellPadding = optPaddingWithFallback(headerCellSection, cellSection, density)
315
+
316
+ // Header/body rows layer on top of the shared tableRow base.
317
+ val rowBase = LayoutStyleSpec.from(styles.rawSection("tableRow"), PLAIN_LAYOUT)
318
+ val headerRowStyle =
319
+ LayoutStyleSpec.from(styles.rawSection("tableHeaderRow"), rowBase).scaled(density)
320
+ val bodyRowStyle =
321
+ LayoutStyleSpec.from(styles.rawSection("tableBodyRow"), rowBase).scaled(density)
322
+
323
+ val rows = ArrayList<Block.TableRowData>()
324
+ for (rowNode in node.children) {
325
+ if (rowNode.type != MdNodeType.TABLE_ROW) {
326
+ continue
327
+ }
328
+ val isHeader = rowNode.level == 1
329
+ val cells = rowNode.children
330
+ .filter { it.type == MdNodeType.TABLE_CELL }
331
+ .map { cell ->
332
+ val builder = SpannableStringBuilder()
333
+ val attrs = if (isHeader) {
334
+ context.apply(
335
+ context.apply(cellAttrs.copy(weight = 700), "tableCell"),
336
+ "tableHeaderCell",
337
+ )
338
+ } else {
339
+ cellAttrs
340
+ }
341
+ walk(builder, cell, attrs, context)
342
+ builder
343
+ }
344
+ rows.add(Block.TableRowData(isHeader, cells))
345
+ }
346
+
347
+ return Block.Table(
348
+ rows = rows,
349
+ cellPaint = basePaint(cellAttrs),
350
+ style = context.layoutStyle("table", LayoutStyleSpec.EMPTY),
351
+ headerRowStyle = headerRowStyle,
352
+ bodyRowStyle = bodyRowStyle,
353
+ cellPaddingLeftPx = cellPadding[0],
354
+ cellPaddingRightPx = cellPadding[1],
355
+ cellPaddingTopPx = cellPadding[2],
356
+ cellPaddingBottomPx = cellPadding[3],
357
+ headerCellPaddingLeftPx = headerCellPadding[0],
358
+ headerCellPaddingRightPx = headerCellPadding[1],
359
+ headerCellPaddingTopPx = headerCellPadding[2],
360
+ headerCellPaddingBottomPx = headerCellPadding[3],
361
+ // Unstyled columns take their natural widths; defaultStyles provides
362
+ // the classic [44, 320] clamps.
363
+ minColumnWidthPx = (tableSection?.optDpOr("minColumnWidth", 0f) ?: 0f) * density,
364
+ maxColumnWidthPx = (tableSection?.optDpOr("maxColumnWidth", 0f) ?: 0f) * density,
365
+ )
366
+ }
367
+
368
+ /** Like optPadding but each key falls back to a second section. */
369
+ private fun optPaddingWithFallback(
370
+ primary: JSONObject?,
371
+ fallback: JSONObject?,
372
+ density: Float,
373
+ ): FloatArray {
374
+ fun side(key: String): Float {
375
+ val fromPrimary = primary?.optDpOr(key, Float.NaN) ?: Float.NaN
376
+ if (!fromPrimary.isNaN()) {
377
+ return fromPrimary * density
378
+ }
379
+ return (fallback?.optDpOr(key, 0f) ?: 0f) * density
380
+ }
381
+ return floatArrayOf(
382
+ side("paddingLeft"),
383
+ side("paddingRight"),
384
+ side("paddingTop"),
385
+ side("paddingBottom"),
386
+ )
387
+ }
388
+
389
+ /** [left, right, top, bottom] px with shorthand expansion done JS-side. */
390
+ private fun JSONObject?.optPadding(density: Float, defaultAll: Float): FloatArray {
391
+ return floatArrayOf(
392
+ (this?.optDpOr("paddingLeft", defaultAll) ?: defaultAll) * density,
393
+ (this?.optDpOr("paddingRight", defaultAll) ?: defaultAll) * density,
394
+ (this?.optDpOr("paddingTop", defaultAll) ?: defaultAll) * density,
395
+ (this?.optDpOr("paddingBottom", defaultAll) ?: defaultAll) * density,
396
+ )
397
+ }
398
+
399
+ private fun merge(base: TextStyleSpec?, over: TextStyleSpec?): TextStyleSpec? {
400
+ if (base == null) {
401
+ return over
402
+ }
403
+ if (over == null) {
404
+ return base
405
+ }
406
+ return TextStyleSpec(
407
+ fontSize = over.fontSize ?: base.fontSize,
408
+ fontWeight = over.fontWeight ?: base.fontWeight,
409
+ fontFamily = over.fontFamily ?: base.fontFamily,
410
+ lineHeight = over.lineHeight ?: base.lineHeight,
411
+ color = over.color ?: base.color,
412
+ fontVariant = over.fontVariant ?: base.fontVariant,
413
+ textDecorationColor = over.textDecorationColor ?: base.textDecorationColor,
414
+ textDecorationLine = over.textDecorationLine ?: base.textDecorationLine,
415
+ textDecorationStyle = over.textDecorationStyle ?: base.textDecorationStyle,
416
+ backgroundColor = over.backgroundColor ?: base.backgroundColor,
417
+ )
418
+ }
419
+
420
+ private fun textBlock(node: MdNode, context: Context, inherited: TextStyleSpec?): Block.Text {
421
+ val builder = SpannableStringBuilder()
422
+ val attrs = baseAttrs(node, context, inherited)
423
+ walk(builder, node, attrs, context)
424
+ val spoilerSection = context.styles.rawSection("spoiler")
425
+ return Block.Text(
426
+ builder,
427
+ basePaint(attrs),
428
+ // Neutral functional floor — the cover must hide text even unstyled;
429
+ // defaultStyles provides the styled cover.
430
+ spoilerColor = spoilerSection?.let { com.jetmarkdown.style.TextStyleSpec.from(it)?.backgroundColor }
431
+ ?: Color.BLACK,
432
+ spoilerRadiusPx = (spoilerSection?.optDpOr("borderRadius", 0f) ?: 0f) * context.density,
433
+ )
434
+ }
435
+
436
+ // codeBlock/blockQuote merge text + layout keys in one section; their
437
+ // backgroundColor is the box fill, not an inline-run background, so it
438
+ // must not reach the text-attribute path.
439
+ private fun textStyleWithoutBackground(styles: StyleConfig, key: String): TextStyleSpec? {
440
+ val section = styles.rawSection(key) ?: return null
441
+ if (!section.has("backgroundColor")) {
442
+ return TextStyleSpec.from(section)
443
+ }
444
+ val copy = JSONObject(section.toString())
445
+ copy.remove("backgroundColor")
446
+ return TextStyleSpec.from(copy)
447
+ }
448
+
449
+ private fun basePaint(attrs: ResolvedAttrs): TextPaint = TextPaint().apply {
450
+ isAntiAlias = true
451
+ textSize = attrs.fontSizePx
452
+ }
453
+
454
+ private fun baseAttrs(node: MdNode, context: Context, inherited: TextStyleSpec?): ResolvedAttrs {
455
+ val scale = context.density * context.fontScale
456
+ var attrs: ResolvedAttrs
457
+ // Unstyled output is fully plain: heading sizes/weights come from the
458
+ // hN sections (defaultStyles on the JS side), not builtins.
459
+ if (node.type == MdNodeType.HEADING) {
460
+ attrs = ResolvedAttrs(fontSizePx = 16f * scale)
461
+ attrs = context.apply(attrs, "base")
462
+ attrs = context.apply(attrs, inherited)
463
+ // Headings shed the inherited lineHeight like they shed paragraph
464
+ // styles: a body lineHeight would cap the taller heading's line box
465
+ // and clip its ascenders. hN.lineHeight still applies.
466
+ attrs = attrs.copy(lineHeightPx = 0)
467
+ attrs = context.apply(attrs, "h${node.level}")
468
+ } else {
469
+ attrs = ResolvedAttrs(fontSizePx = 16f * scale)
470
+ attrs = context.apply(attrs, "base")
471
+ attrs = context.apply(attrs, "paragraph")
472
+ attrs = context.apply(attrs, inherited)
473
+ }
474
+ return attrs
475
+ }
476
+
477
+ private fun walk(
478
+ builder: SpannableStringBuilder,
479
+ parent: MdNode,
480
+ attrs: ResolvedAttrs,
481
+ context: Context,
482
+ ) {
483
+ for (node in parent.children) {
484
+ when (node.type) {
485
+ MdNodeType.TEXT -> appendRun(builder, node.text, attrs)
486
+ MdNodeType.SOFT_BREAK -> appendRun(builder, " ", attrs)
487
+ MdNodeType.HARD_BREAK -> appendRun(builder, "\n", attrs)
488
+ MdNodeType.BOLD ->
489
+ walk(builder, node, context.apply(attrs.copy(weight = 700), "bold"), context)
490
+ MdNodeType.ITALIC ->
491
+ walk(builder, node, context.apply(attrs.copy(italic = true), "italic"), context)
492
+ MdNodeType.STRIKETHROUGH ->
493
+ walk(
494
+ builder,
495
+ node,
496
+ context.apply(attrs.copy(strikethrough = true), "strikethrough"),
497
+ context,
498
+ )
499
+ MdNodeType.LINK -> {
500
+ var next = attrs.copy(linkUrl = node.url)
501
+ val variant = context.styles.mentionVariants.firstOrNull {
502
+ it.pattern.matcher(node.url).find()
503
+ }
504
+ val sectionKey = if (variant != null) "mention" else "link"
505
+ next = if (variant != null) {
506
+ context.apply(context.apply(next, "mention"), variant.style)
507
+ } else {
508
+ context.apply(next, "link")
509
+ }
510
+ val section = context.styles.rawSection(sectionKey)
511
+ if (section != null) {
512
+ next = next.copy(
513
+ chipRadiusPx =
514
+ (section.optDpOr("borderRadius", 0f)) * context.density,
515
+ )
516
+ }
517
+ val nodeChipColor = next.backgroundColor
518
+ if (nodeChipColor != null) {
519
+ val chipStart = builder.length
520
+ walk(builder, node, next.copy(suppressRunChip = true), context)
521
+ if (builder.length > chipStart) {
522
+ builder.setSpan(
523
+ ChipSpan(
524
+ color = nodeChipColor,
525
+ radiusPx = next.chipRadiusPx,
526
+ padLeftPx = next.chipPadLeftPx,
527
+ padRightPx = next.chipPadRightPx,
528
+ typeface = buildTypeface(next),
529
+ textSizePx = next.fontSizePx,
530
+ ),
531
+ chipStart,
532
+ builder.length,
533
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
534
+ )
535
+ }
536
+ } else {
537
+ walk(builder, node, next, context)
538
+ }
539
+ }
540
+ MdNodeType.INLINE_CODE -> {
541
+ var next = attrs.copy(family = "monospace")
542
+ next = context.apply(next, "inlineCode")
543
+ val section = context.styles.rawSection("inlineCode")
544
+ if (section != null) {
545
+ next = next.copy(
546
+ chipRadiusPx =
547
+ (section.optDpOr("borderRadius", 0f)) * context.density,
548
+ chipPadLeftPx =
549
+ (section.optDpOr("paddingLeft", 0f)) * context.density,
550
+ chipPadRightPx =
551
+ (section.optDpOr("paddingRight", 0f)) * context.density,
552
+ )
553
+ }
554
+ appendRun(builder, node.text, next)
555
+ }
556
+ MdNodeType.SUPERSCRIPT -> {
557
+ var next = attrs.copy(
558
+ fontSizePx = attrs.fontSizePx * 0.7f,
559
+ baselineShiftPx = (-attrs.fontSizePx * 0.35f).toInt(),
560
+ )
561
+ next = context.apply(next, "superscript")
562
+ walk(builder, node, next, context)
563
+ }
564
+ MdNodeType.SUBSCRIPT -> {
565
+ var next = attrs.copy(
566
+ fontSizePx = attrs.fontSizePx * 0.7f,
567
+ baselineShiftPx = (attrs.fontSizePx * 0.18f).toInt(),
568
+ )
569
+ next = context.apply(next, "subscript")
570
+ walk(builder, node, next, context)
571
+ }
572
+ MdNodeType.SPOILER ->
573
+ walk(builder, node, attrs.copy(spoilerId = context.nextSpoilerId()), context)
574
+ MdNodeType.IMAGE -> appendRun(builder, node.text, attrs)
575
+ else -> walk(builder, node, attrs, context)
576
+ }
577
+ }
578
+ }
579
+
580
+ private fun appendRun(builder: SpannableStringBuilder, text: String, attrs: ResolvedAttrs) {
581
+ if (text.isEmpty()) {
582
+ return
583
+ }
584
+ val start = builder.length
585
+ builder.append(text)
586
+ val typeface = buildTypeface(attrs)
587
+ builder.setSpan(
588
+ RunSpan(
589
+ typeface = typeface,
590
+ textSizePx = attrs.fontSizePx,
591
+ color = attrs.color,
592
+ baselineShiftPx = attrs.baselineShiftPx,
593
+ fontFeatureSettings = attrs.variants?.let(::featureSettings),
594
+ underline = attrs.underline,
595
+ strikethrough = attrs.strikethrough,
596
+ ),
597
+ start,
598
+ builder.length,
599
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
600
+ )
601
+ if (attrs.backgroundColor != null && !attrs.suppressRunChip) {
602
+ builder.setSpan(
603
+ ChipSpan(
604
+ color = attrs.backgroundColor,
605
+ radiusPx = attrs.chipRadiusPx,
606
+ padLeftPx = attrs.chipPadLeftPx,
607
+ padRightPx = attrs.chipPadRightPx,
608
+ typeface = typeface,
609
+ textSizePx = attrs.fontSizePx,
610
+ ),
611
+ start,
612
+ builder.length,
613
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
614
+ )
615
+ }
616
+ if (attrs.lineHeightPx > 0) {
617
+ builder.setSpan(
618
+ MarkdownLineHeightSpan(attrs.lineHeightPx),
619
+ start,
620
+ builder.length,
621
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
622
+ )
623
+ }
624
+ if (attrs.linkUrl != null) {
625
+ builder.setSpan(LinkSpan(attrs.linkUrl), start, builder.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
626
+ }
627
+ if (attrs.spoilerId != null) {
628
+ builder.setSpan(
629
+ SpoilerSpan(attrs.spoilerId, typeface, attrs.fontSizePx),
630
+ start,
631
+ builder.length,
632
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
633
+ )
634
+ }
635
+ }
636
+
637
+ private fun buildTypeface(attrs: ResolvedAttrs): Typeface =
638
+ Fonts.resolve(PlatformColorResolver.current(), attrs.family, attrs.weight, attrs.italic)
639
+
640
+ private fun featureSettings(variants: List<String>): String? {
641
+ val features = variants.mapNotNull {
642
+ when (it) {
643
+ "tabular-nums" -> "'tnum'"
644
+ "proportional-nums" -> "'pnum'"
645
+ "oldstyle-nums" -> "'onum'"
646
+ "lining-nums" -> "'lnum'"
647
+ "small-caps" -> "'smcp'"
648
+ else -> null
649
+ }
650
+ }
651
+ return if (features.isEmpty()) null else features.joinToString(", ")
652
+ }
653
+
654
+ private fun JSONObject.optDpOr(key: String, fallback: Float): Float {
655
+ val value = optDouble(key)
656
+ return if (value.isNaN()) fallback else value.toFloat()
657
+ }
658
+ }
@@ -0,0 +1,17 @@
1
+ package com.jetmarkdown.render.spans
2
+
3
+ /**
4
+ * Data-only marker for a drawn run background ("chip"): BlockTextView fills
5
+ * a rounded rect behind the run, sized from the run's real font metrics so
6
+ * ascenders and descenders are always covered (TextPaint.bgColor and
7
+ * NSBackgroundColor both misalign under custom line heights).
8
+ */
9
+ class ChipSpan(
10
+ val color: Int,
11
+ val radiusPx: Float,
12
+ val padLeftPx: Float,
13
+ val padRightPx: Float,
14
+ /** Run font, for measuring glyph ink bounds at draw time. */
15
+ val typeface: android.graphics.Typeface?,
16
+ val textSizePx: Float,
17
+ )
@@ -0,0 +1,11 @@
1
+ package com.jetmarkdown.render.spans
2
+
3
+ /** Data-only marker spans; hit-testing and drawing live in BlockTextView. */
4
+ class LinkSpan(val url: String)
5
+
6
+ class SpoilerSpan(
7
+ val id: Int,
8
+ /** Run font, for measuring glyph ink bounds at draw time. */
9
+ val typeface: android.graphics.Typeface?,
10
+ val textSizePx: Float,
11
+ )