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,59 @@
1
+ package com.jetmarkdown.render.spans
2
+
3
+ import android.graphics.Paint
4
+ import android.text.TextPaint
5
+ import android.text.style.LineHeightSpan
6
+ import android.text.style.MetricAffectingSpan
7
+ import kotlin.math.ceil
8
+ import kotlin.math.floor
9
+
10
+ /**
11
+ * React Native's lineHeight semantics: the line box is exactly [heightPx]
12
+ * tall with glyphs centered (mirrors RN's CustomLineHeightSpan). Extends
13
+ * MetricAffectingSpan (a no-op there) so DynamicLayout — the editor —
14
+ * reflows when the span is attached; plain LineHeightSpan is not an
15
+ * UpdateLayout span.
16
+ */
17
+ open class MarkdownLineHeightSpan(private val heightPx: Int) :
18
+ MetricAffectingSpan(),
19
+ LineHeightSpan {
20
+ override fun updateMeasureState(paint: TextPaint) = Unit
21
+
22
+ override fun updateDrawState(paint: TextPaint) = Unit
23
+
24
+ override fun chooseHeight(
25
+ text: CharSequence,
26
+ start: Int,
27
+ end: Int,
28
+ spanstartv: Int,
29
+ lineHeight: Int,
30
+ fm: Paint.FontMetricsInt,
31
+ ) {
32
+ if (fm.descent > heightPx) {
33
+ // Show as much descent as possible.
34
+ fm.descent = minOf(heightPx, fm.descent)
35
+ fm.bottom = fm.descent
36
+ fm.ascent = 0
37
+ fm.top = 0
38
+ } else if (-fm.ascent + fm.descent > heightPx) {
39
+ // Keep the descent, crop the ascent.
40
+ fm.bottom = fm.descent
41
+ fm.ascent = -heightPx + fm.descent
42
+ fm.top = fm.ascent
43
+ } else if (-fm.ascent + fm.bottom > heightPx) {
44
+ // Crop the additional bottom padding.
45
+ fm.top = fm.ascent
46
+ fm.bottom = fm.ascent + heightPx
47
+ } else if (-fm.top + fm.bottom > heightPx) {
48
+ // Crop the additional top padding.
49
+ fm.top = fm.bottom - heightPx
50
+ } else {
51
+ // Center the glyphs in the taller line box.
52
+ val extra = (heightPx - (-fm.top + fm.bottom)) / 2.0
53
+ fm.top -= ceil(extra).toInt()
54
+ fm.bottom += floor(extra).toInt()
55
+ fm.ascent = fm.top
56
+ fm.descent = fm.bottom
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,40 @@
1
+ package com.jetmarkdown.render.spans
2
+
3
+ import android.graphics.Typeface
4
+ import android.text.TextPaint
5
+ import android.text.style.MetricAffectingSpan
6
+
7
+ /**
8
+ * One span per text run carrying the fully-resolved attributes, applied to
9
+ * both measurement and drawing so StaticLayout heights stay exact.
10
+ */
11
+ class RunSpan(
12
+ private val typeface: Typeface,
13
+ private val textSizePx: Float,
14
+ private val color: Int,
15
+ private val baselineShiftPx: Int,
16
+ private val fontFeatureSettings: String?,
17
+ private val underline: Boolean,
18
+ private val strikethrough: Boolean,
19
+ ) : MetricAffectingSpan() {
20
+
21
+ override fun updateMeasureState(paint: TextPaint) {
22
+ applyMetrics(paint)
23
+ }
24
+
25
+ override fun updateDrawState(paint: TextPaint) {
26
+ applyMetrics(paint)
27
+ paint.color = color
28
+ paint.isUnderlineText = underline
29
+ paint.isStrikeThruText = strikethrough
30
+ }
31
+
32
+ private fun applyMetrics(paint: TextPaint) {
33
+ paint.typeface = typeface
34
+ paint.textSize = textSizePx
35
+ paint.baselineShift += baselineShiftPx
36
+ if (fontFeatureSettings != null) {
37
+ paint.fontFeatureSettings = fontFeatureSettings
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,34 @@
1
+ package com.jetmarkdown.style
2
+
3
+ import android.content.Context
4
+ import android.graphics.Typeface
5
+ import android.os.Build
6
+ import com.facebook.react.common.assets.ReactFontManager
7
+
8
+ /**
9
+ * Typeface resolution that understands React Native custom fonts.
10
+ * Typeface.create() only knows system families; fonts registered through
11
+ * RN/Expo (assets/fonts, expo-font) resolve via ReactFontManager — the same
12
+ * path RN's own <Text> uses.
13
+ */
14
+ object Fonts {
15
+ fun resolve(context: Context?, family: String?, weight: Int, italic: Boolean): Typeface {
16
+ val style = when {
17
+ weight >= 600 && italic -> Typeface.BOLD_ITALIC
18
+ weight >= 600 -> Typeface.BOLD
19
+ italic -> Typeface.ITALIC
20
+ else -> Typeface.NORMAL
21
+ }
22
+ val base = when {
23
+ family == null -> Typeface.DEFAULT
24
+ context != null ->
25
+ ReactFontManager.getInstance().getTypeface(family, style, context.assets)
26
+ else -> Typeface.create(family, style)
27
+ }
28
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
29
+ Typeface.create(base, weight, italic)
30
+ } else {
31
+ Typeface.create(base, style)
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,71 @@
1
+ package com.jetmarkdown.style
2
+
3
+ import com.jetmarkdown.style.TextStyleSpec.Companion.optColor
4
+ import org.json.JSONObject
5
+
6
+ /** One element's box style from stylesJson (dp values). */
7
+ class LayoutStyleSpec(
8
+ val backgroundColor: Int?,
9
+ val paddingLeft: Float,
10
+ val paddingRight: Float,
11
+ val paddingTop: Float,
12
+ val paddingBottom: Float,
13
+ val borderRadius: Float,
14
+ val borderLeftColor: Int?,
15
+ val borderLeftWidth: Float,
16
+ val borderRightColor: Int?,
17
+ val borderRightWidth: Float,
18
+ val borderTopColor: Int?,
19
+ val borderTopWidth: Float,
20
+ val borderBottomColor: Int?,
21
+ val borderBottomWidth: Float,
22
+ ) {
23
+ /** Converts dp values to px; colors unchanged. */
24
+ fun scaled(density: Float): LayoutStyleSpec = LayoutStyleSpec(
25
+ backgroundColor = backgroundColor,
26
+ paddingLeft = paddingLeft * density,
27
+ paddingRight = paddingRight * density,
28
+ paddingTop = paddingTop * density,
29
+ paddingBottom = paddingBottom * density,
30
+ borderRadius = borderRadius * density,
31
+ borderLeftColor = borderLeftColor,
32
+ borderLeftWidth = borderLeftWidth * density,
33
+ borderRightColor = borderRightColor,
34
+ borderRightWidth = borderRightWidth * density,
35
+ borderTopColor = borderTopColor,
36
+ borderTopWidth = borderTopWidth * density,
37
+ borderBottomColor = borderBottomColor,
38
+ borderBottomWidth = borderBottomWidth * density,
39
+ )
40
+
41
+ companion object {
42
+ val EMPTY = LayoutStyleSpec(null, 0f, 0f, 0f, 0f, 0f, null, 0f, null, 0f, null, 0f, null, 0f)
43
+
44
+ fun from(json: JSONObject?, defaults: LayoutStyleSpec = EMPTY): LayoutStyleSpec {
45
+ if (json == null) {
46
+ return defaults
47
+ }
48
+ return LayoutStyleSpec(
49
+ backgroundColor = json.optColor("backgroundColor") ?: defaults.backgroundColor,
50
+ paddingLeft = json.optDp("paddingLeft", defaults.paddingLeft),
51
+ paddingRight = json.optDp("paddingRight", defaults.paddingRight),
52
+ paddingTop = json.optDp("paddingTop", defaults.paddingTop),
53
+ paddingBottom = json.optDp("paddingBottom", defaults.paddingBottom),
54
+ borderRadius = json.optDp("borderRadius", defaults.borderRadius),
55
+ borderLeftColor = json.optColor("borderLeftColor") ?: defaults.borderLeftColor,
56
+ borderLeftWidth = json.optDp("borderLeftWidth", defaults.borderLeftWidth),
57
+ borderRightColor = json.optColor("borderRightColor") ?: defaults.borderRightColor,
58
+ borderRightWidth = json.optDp("borderRightWidth", defaults.borderRightWidth),
59
+ borderTopColor = json.optColor("borderTopColor") ?: defaults.borderTopColor,
60
+ borderTopWidth = json.optDp("borderTopWidth", defaults.borderTopWidth),
61
+ borderBottomColor = json.optColor("borderBottomColor") ?: defaults.borderBottomColor,
62
+ borderBottomWidth = json.optDp("borderBottomWidth", defaults.borderBottomWidth),
63
+ )
64
+ }
65
+
66
+ private fun JSONObject.optDp(key: String, fallback: Float): Float {
67
+ val value = optDouble(key)
68
+ return if (value.isNaN()) fallback else value.toFloat()
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,77 @@
1
+ package com.jetmarkdown.style
2
+
3
+ import android.content.Context
4
+ import android.util.TypedValue
5
+ import java.lang.ref.WeakReference
6
+ import org.json.JSONObject
7
+
8
+ /**
9
+ * Resolves React Native platform-color descriptors ({"resource_paths":
10
+ * ["@color/x", "?attr/y", ...]}) against the current theme. The context is
11
+ * registered by the view managers (a ThemedReactContext, so ?attr lookups
12
+ * see the activity theme) and held weakly.
13
+ */
14
+ object PlatformColorResolver {
15
+ @Volatile private var contextRef: WeakReference<Context>? = null
16
+
17
+ fun install(context: Context) {
18
+ contextRef = WeakReference(context)
19
+ }
20
+
21
+ /** The registered themed context (font/color resolution off-view). */
22
+ fun current(): Context? = contextRef?.get()
23
+
24
+ /** Night-mode bit of the registered context; part of style cache keys. */
25
+ fun appearanceKey(): Int {
26
+ val context = contextRef?.get() ?: return 0
27
+ return context.resources.configuration.uiMode and
28
+ android.content.res.Configuration.UI_MODE_NIGHT_MASK
29
+ }
30
+
31
+ fun resolve(json: JSONObject): Int? {
32
+ val paths = json.optJSONArray("resource_paths") ?: return null
33
+ val context = contextRef?.get() ?: return null
34
+ for (index in 0 until paths.length()) {
35
+ val resolved = resolvePath(context, paths.optString(index, ""))
36
+ if (resolved != null) {
37
+ return resolved
38
+ }
39
+ }
40
+ return null
41
+ }
42
+
43
+ private fun resolvePath(context: Context, path: String): Int? {
44
+ if (path.isEmpty()) {
45
+ return null
46
+ }
47
+ if (path.startsWith("?")) {
48
+ val name = path.removePrefix("?").removePrefix("attr/")
49
+ val attrId = context.resources.getIdentifier(name, "attr", context.packageName)
50
+ if (attrId == 0) {
51
+ return null
52
+ }
53
+ val out = TypedValue()
54
+ if (!context.theme.resolveAttribute(attrId, out, true)) {
55
+ return null
56
+ }
57
+ if (out.type in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT) {
58
+ return out.data
59
+ }
60
+ return runCatching {
61
+ context.resources.getColor(out.resourceId, context.theme)
62
+ }.getOrNull()
63
+ }
64
+ if (path.startsWith("@")) {
65
+ // "@color/name" or "@android:color/name".
66
+ val spec = path.removePrefix("@")
67
+ val pkg = if (spec.contains(":")) spec.substringBefore(":") else context.packageName
68
+ val name = spec.substringAfter(":").removePrefix("color/")
69
+ val id = context.resources.getIdentifier(name, "color", pkg)
70
+ if (id == 0) {
71
+ return null
72
+ }
73
+ return runCatching { context.resources.getColor(id, context.theme) }.getOrNull()
74
+ }
75
+ return null
76
+ }
77
+ }
@@ -0,0 +1,80 @@
1
+ package com.jetmarkdown.style
2
+
3
+ import com.jetmarkdown.style.TextStyleSpec.Companion.optColor
4
+ import java.util.regex.Pattern
5
+ import org.json.JSONObject
6
+
7
+ class MentionVariant(val pattern: Pattern, val style: TextStyleSpec?)
8
+
9
+ /**
10
+ * Parsed stylesJson with defaults, cached per JSON string.
11
+ */
12
+ class StyleConfig private constructor(private val root: JSONObject) {
13
+ private val main: JSONObject? = root.optJSONObject("main")
14
+ private val textStyles = HashMap<String, TextStyleSpec?>()
15
+
16
+ // Container style-prop gap wins; the styles prop (defaultStyles.gap)
17
+ // supplies the themed value; unstyled floor is 0.
18
+ val gap: Float = main.optFloatOr("gap", root.optFloatOr("gap", 0f))
19
+ val paddingLeft: Float = main.optFloatOr("paddingLeft", 0f)
20
+ val paddingRight: Float = main.optFloatOr("paddingRight", 0f)
21
+ val paddingTop: Float = main.optFloatOr("paddingTop", 0f)
22
+ val paddingBottom: Float = main.optFloatOr("paddingBottom", 0f)
23
+ val backgroundColor: Int? = main?.optColor("backgroundColor")
24
+
25
+ /** Ordered longest-pattern-first; a link whose URL matches is a mention. */
26
+ val mentionVariants: List<MentionVariant> = buildList {
27
+ val variants = root.optJSONObject("mention")?.optJSONArray("variants") ?: return@buildList
28
+ for (i in 0 until variants.length()) {
29
+ val pair = variants.optJSONArray(i) ?: continue
30
+ val patternString = pair.optString(0, "")
31
+ if (patternString.isEmpty()) {
32
+ continue
33
+ }
34
+ val pattern = runCatching { Pattern.compile(patternString) }.getOrNull() ?: continue
35
+ add(MentionVariant(pattern, TextStyleSpec.from(pair.optJSONObject(1))))
36
+ }
37
+ }
38
+
39
+ /** Raw JSON section for an element key (layout parsing). */
40
+ fun rawSection(key: String): JSONObject? = root.optJSONObject(key)
41
+
42
+ /** User style for an element key; null when not provided. */
43
+ fun textStyleFor(key: String): TextStyleSpec? {
44
+ synchronized(textStyles) {
45
+ if (textStyles.containsKey(key)) {
46
+ return textStyles[key]
47
+ }
48
+ val style = TextStyleSpec.from(root.optJSONObject(key))
49
+ textStyles[key] = style
50
+ return style
51
+ }
52
+ }
53
+
54
+
55
+ companion object {
56
+ private val cache = HashMap<String, StyleConfig>()
57
+
58
+ fun from(stylesJson: String): StyleConfig {
59
+ // Platform colors resolve against the current theme; a dark-mode flip
60
+ // must not serve configs holding light-resolved ints.
61
+ val cacheKey = "${PlatformColorResolver.appearanceKey()}\u001f$stylesJson"
62
+ synchronized(cache) {
63
+ cache[cacheKey]?.let { return it }
64
+ val config = StyleConfig(
65
+ runCatching { JSONObject(stylesJson.ifEmpty { "{}" }) }.getOrElse { JSONObject() }
66
+ )
67
+ if (cache.size > 16) {
68
+ cache.clear()
69
+ }
70
+ cache[cacheKey] = config
71
+ return config
72
+ }
73
+ }
74
+
75
+ private fun JSONObject?.optFloatOr(key: String, fallback: Float): Float {
76
+ val value = this?.optDouble(key) ?: return fallback
77
+ return if (value.isNaN()) fallback else value.toFloat()
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,61 @@
1
+ package com.jetmarkdown.style
2
+
3
+ import org.json.JSONArray
4
+ import org.json.JSONObject
5
+
6
+ /** One element's text style from stylesJson; null fields inherit. */
7
+ class TextStyleSpec(
8
+ val fontSize: Float?,
9
+ val fontWeight: Int?,
10
+ val fontFamily: String?,
11
+ val lineHeight: Float?,
12
+ val color: Int?,
13
+ val fontVariant: List<String>?,
14
+ val textDecorationColor: Int?,
15
+ val textDecorationLine: String?,
16
+ val textDecorationStyle: String?,
17
+ val backgroundColor: Int?,
18
+ ) {
19
+ companion object {
20
+ fun from(json: JSONObject?): TextStyleSpec? {
21
+ if (json == null) {
22
+ return null
23
+ }
24
+ return TextStyleSpec(
25
+ fontSize = json.optNumber("fontSize"),
26
+ fontWeight = when (val weight = json.optString("fontWeight", "")) {
27
+ "" -> null
28
+ "bold" -> 700
29
+ "normal" -> 400
30
+ else -> weight.toIntOrNull()?.takeIf { it in 100..900 }
31
+ },
32
+ fontFamily = json.optString("fontFamily", "").ifEmpty { null },
33
+ lineHeight = json.optNumber("lineHeight"),
34
+ color = json.optColor("color"),
35
+ fontVariant = (json.optJSONArray("fontVariant"))?.toStringList(),
36
+ textDecorationColor = json.optColor("textDecorationColor"),
37
+ textDecorationLine = json.optString("textDecorationLine", "").ifEmpty { null },
38
+ textDecorationStyle = json.optString("textDecorationStyle", "").ifEmpty { null },
39
+ backgroundColor = json.optColor("backgroundColor"),
40
+ )
41
+ }
42
+
43
+ private fun JSONObject.optNumber(key: String): Float? {
44
+ val value = optDouble(key)
45
+ return if (value.isNaN()) null else value.toFloat()
46
+ }
47
+
48
+ internal fun JSONObject.optColor(key: String): Int? {
49
+ if (!has(key)) {
50
+ return null
51
+ }
52
+ // Platform colors arrive as {"resource_paths": [...]} descriptors.
53
+ optJSONObject(key)?.let { return PlatformColorResolver.resolve(it) }
54
+ val value = optLong(key, Long.MIN_VALUE)
55
+ return if (value == Long.MIN_VALUE) null else value.toInt()
56
+ }
57
+
58
+ private fun JSONArray.toStringList(): List<String> =
59
+ (0 until length()).mapNotNull { optString(it, null) }
60
+ }
61
+ }