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