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