react-native-enriched-markdown-scaffold 0.7.4-scaffold.1 → 0.7.4-scaffold.11
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/android/build.gradle +5 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerDelegate.java +9 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerInterface.java +3 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +10 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +7 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +10 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +50 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +6 -4
- package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt +38 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +174 -9
- package/android/src/main/java/com/swmansion/enriched/markdown/input/events/OnPasteEvent.kt +24 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/BlockSyntaxSentinel.kt +69 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputParser.kt +71 -12
- package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputEventEmitter.kt +8 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputLayoutManager.kt +30 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputMeasurementStore.kt +7 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/input/model/MathChipStyle.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/HeadingSpan.kt +8 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +27 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineMathStyle.kt +5 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/MathFontWeight.kt +25 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/MathStyle.kt +12 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +7 -7
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/MarkdownStyleParser.kt +10 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/PasteTextUtils.kt +20 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/extensions/SpannableExtensions.kt +12 -4
- package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +1 -1
- package/android/src/math/java/com/swmansion/enriched/markdown/renderer/MathInlineRenderer.kt +2 -0
- package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathChipSpan.kt +1 -1
- package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathInlinePlaceholderSpan.kt +7 -3
- package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathInlineSpan.kt +8 -5
- package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathTypeface.kt +30 -0
- package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +3 -1
- package/docs/API_REFERENCE.md +43 -0
- package/docs/INPUT.md +87 -0
- package/docs/LATEX_MATH.md +62 -0
- package/docs/STYLES.md +3 -0
- package/ios/EnrichedMarkdown.mm +44 -12
- package/ios/EnrichedMarkdownText.mm +47 -11
- package/ios/attachments/ENRMMathInlineAttachment+macOS.m +5 -1
- package/ios/attachments/ENRMMathInlineAttachment.h +3 -0
- package/ios/attachments/ENRMMathInlineAttachment.m +2 -1
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +10 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +7 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +10 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +50 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +29 -0
- package/ios/input/ENRMBlockSyntaxSentinel.h +25 -0
- package/ios/input/ENRMBlockSyntaxSentinel.mm +80 -0
- package/ios/input/ENRMInputFormatter.h +4 -0
- package/ios/input/ENRMInputFormatter.mm +2 -0
- package/ios/input/ENRMInputParser.h +5 -0
- package/ios/input/ENRMInputParser.mm +79 -6
- package/ios/input/ENRMInputTextView.mm +11 -1
- package/ios/input/EnrichedMarkdownTextInput.h +5 -0
- package/ios/input/EnrichedMarkdownTextInput.mm +88 -22
- package/ios/input/InputStylePropsUtils.h +13 -0
- package/ios/internals/MeasurementCache.h +1 -0
- package/ios/math/ENRMRaTeXBridge.swift +52 -3
- package/ios/parser/ENRMMarkdownParser.h +8 -0
- package/ios/parser/ENRMMarkdownParser.mm +11 -0
- package/ios/renderer/ENRMMathInlineRenderer.m +3 -0
- package/ios/renderer/ListItemRenderer.m +8 -5
- package/ios/styles/StyleConfig.h +6 -0
- package/ios/styles/StyleConfig.mm +36 -0
- package/ios/utils/ENRMMathFontWeight.h +18 -0
- package/ios/utils/StylePropsUtils.h +21 -0
- package/ios/views/ENRMMathContainerView.m +3 -1
- package/lib/module/EnrichedMarkdownNativeComponent.ts +3 -0
- package/lib/module/EnrichedMarkdownTextInput.js +17 -0
- package/lib/module/EnrichedMarkdownTextInput.js.map +1 -1
- package/lib/module/EnrichedMarkdownTextInputNativeComponent.ts +34 -0
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +3 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/normalizeMarkdownStyle.js +5 -2
- package/lib/module/normalizeMarkdownStyle.js.map +1 -1
- package/lib/module/normalizeMarkdownStyle.web.js +5 -2
- package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
- package/lib/module/normalizeMarkdownTextInputStyle.js +7 -3
- package/lib/module/normalizeMarkdownTextInputStyle.js.map +1 -1
- package/lib/module/web/globalStyles.js +8 -1
- package/lib/module/web/globalStyles.js.map +1 -1
- package/lib/module/web/styles.js +3 -1
- package/lib/module/web/styles.js.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +3 -0
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts +26 -1
- package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts +28 -0
- package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +3 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
- package/lib/typescript/src/normalizeMarkdownStyle.web.d.ts.map +1 -1
- package/lib/typescript/src/normalizeMarkdownTextInputStyle.d.ts +1 -0
- package/lib/typescript/src/normalizeMarkdownTextInputStyle.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownStyle.d.ts +32 -0
- package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +3 -0
- package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
- package/lib/typescript/src/types/events.d.ts +15 -0
- package/lib/typescript/src/types/events.d.ts.map +1 -1
- package/lib/typescript/src/web/styles.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/EnrichedMarkdownNativeComponent.ts +3 -0
- package/src/EnrichedMarkdownTextInput.tsx +41 -0
- package/src/EnrichedMarkdownTextInputNativeComponent.ts +34 -0
- package/src/EnrichedMarkdownTextNativeComponent.ts +3 -0
- package/src/index.tsx +1 -0
- package/src/normalizeMarkdownStyle.ts +6 -1
- package/src/normalizeMarkdownStyle.web.ts +6 -1
- package/src/normalizeMarkdownTextInputStyle.ts +5 -0
- package/src/types/MarkdownStyle.ts +32 -0
- package/src/types/MarkdownStyleInternal.ts +3 -0
- package/src/types/events.ts +16 -0
- package/src/web/globalStyles.ts +7 -0
- package/src/web/styles.ts +5 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
package com.swmansion.enriched.markdown.spans
|
|
2
2
|
|
|
3
3
|
import android.graphics.Paint
|
|
4
|
+
import android.text.Spanned
|
|
5
|
+
import android.text.style.ReplacementSpan
|
|
4
6
|
import kotlin.math.ceil
|
|
5
7
|
import kotlin.math.floor
|
|
6
8
|
import android.text.style.LineHeightSpan as AndroidLineHeightSpan
|
|
@@ -18,10 +20,34 @@ class LineHeightSpan(
|
|
|
18
20
|
v: Int,
|
|
19
21
|
fm: Paint.FontMetricsInt?,
|
|
20
22
|
) {
|
|
21
|
-
if (fm == null) return
|
|
23
|
+
if (fm == null || lineHeight <= 0) return
|
|
22
24
|
|
|
23
25
|
val leading = lineHeight - ((-fm.ascent) + fm.descent)
|
|
26
|
+
|
|
27
|
+
// A ReplacementSpan on this line — inline math, or the placeholder MeasurementStore
|
|
28
|
+
// swaps in to measure it off the main thread — reports its own metrics from getSize().
|
|
29
|
+
// Shrinking those back down to the configured line height cuts the line box below what
|
|
30
|
+
// the span actually draws, and can drive descent negative so the next line starts above
|
|
31
|
+
// this one's baseline. Those lines are expanded only, never compressed; every other line
|
|
32
|
+
// keeps the exact clamp it had before. The lookup only runs on the rare negative-leading
|
|
33
|
+
// path, so the common case stays allocation-free.
|
|
34
|
+
if (leading <= 0 && hasSelfMeasuringSpan(text, start, end)) return
|
|
35
|
+
|
|
24
36
|
fm.ascent -= ceil(leading / 2.0f).toInt()
|
|
25
37
|
fm.descent += floor(leading / 2.0f).toInt()
|
|
26
38
|
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Images are excluded on purpose. Block images already keep this span off their range via
|
|
42
|
+
* `applyLineHeightSkippingImages`, and inline ones never touch the metrics at all, so
|
|
43
|
+
* letting them through here would change image layout instead of math layout.
|
|
44
|
+
*/
|
|
45
|
+
private fun hasSelfMeasuringSpan(
|
|
46
|
+
text: CharSequence?,
|
|
47
|
+
start: Int,
|
|
48
|
+
end: Int,
|
|
49
|
+
): Boolean {
|
|
50
|
+
val spanned = text as? Spanned ?: return false
|
|
51
|
+
return spanned.getSpans(start, end, ReplacementSpan::class.java).any { it !is ImageSpan }
|
|
52
|
+
}
|
|
27
53
|
}
|
|
@@ -4,6 +4,8 @@ import com.facebook.react.bridge.ReadableMap
|
|
|
4
4
|
|
|
5
5
|
data class InlineMathStyle(
|
|
6
6
|
val color: Int,
|
|
7
|
+
val paddingVertical: Float,
|
|
8
|
+
val fontWeight: String,
|
|
7
9
|
) {
|
|
8
10
|
companion object {
|
|
9
11
|
fun fromReadableMap(
|
|
@@ -11,7 +13,9 @@ data class InlineMathStyle(
|
|
|
11
13
|
parser: StyleParser,
|
|
12
14
|
): InlineMathStyle {
|
|
13
15
|
val color = parser.parseColor(map, "color")
|
|
14
|
-
|
|
16
|
+
val paddingVertical = parser.toPixelFromDIP(map.getDouble("paddingVertical").toFloat())
|
|
17
|
+
val fontWeight = parser.parseString(map, "fontWeight", "normal")
|
|
18
|
+
return InlineMathStyle(color, paddingVertical, fontWeight)
|
|
15
19
|
}
|
|
16
20
|
}
|
|
17
21
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.styles
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Weight a formula can actually be typeset at.
|
|
5
|
+
*
|
|
6
|
+
* The style model keeps `fontWeight` as a [String], the way every other
|
|
7
|
+
* `fontWeight` prop in the codebase does, so a value like `"600"` survives as
|
|
8
|
+
* far as the renderer. This enum is the narrowing step at that last hop — see
|
|
9
|
+
* `ratexTypefaceLoader`, the only caller — so adding a third weight is a
|
|
10
|
+
* compile error at the draw site rather than a value that silently falls back
|
|
11
|
+
* to regular. Mirrors ENRMMathFontWeight.h on iOS.
|
|
12
|
+
*/
|
|
13
|
+
enum class MathFontWeight {
|
|
14
|
+
NORMAL,
|
|
15
|
+
BOLD,
|
|
16
|
+
;
|
|
17
|
+
|
|
18
|
+
companion object {
|
|
19
|
+
fun fromString(value: String): MathFontWeight =
|
|
20
|
+
when (value.lowercase()) {
|
|
21
|
+
"bold" -> BOLD
|
|
22
|
+
else -> NORMAL
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -10,6 +10,7 @@ data class MathStyle(
|
|
|
10
10
|
val marginTop: Float,
|
|
11
11
|
val marginBottom: Float,
|
|
12
12
|
val textAlign: String,
|
|
13
|
+
val fontWeight: String,
|
|
13
14
|
) {
|
|
14
15
|
companion object {
|
|
15
16
|
fun fromReadableMap(
|
|
@@ -23,7 +24,17 @@ data class MathStyle(
|
|
|
23
24
|
val marginTop = parser.toPixelFromDIP(map.getDouble("marginTop").toFloat())
|
|
24
25
|
val marginBottom = parser.toPixelFromDIP(map.getDouble("marginBottom").toFloat())
|
|
25
26
|
val textAlign = parser.parseString(map, "textAlign", "center")
|
|
26
|
-
|
|
27
|
+
val fontWeight = parser.parseString(map, "fontWeight", "normal")
|
|
28
|
+
return MathStyle(
|
|
29
|
+
fontSize,
|
|
30
|
+
color,
|
|
31
|
+
backgroundColor,
|
|
32
|
+
padding,
|
|
33
|
+
marginTop,
|
|
34
|
+
marginBottom,
|
|
35
|
+
textAlign,
|
|
36
|
+
fontWeight,
|
|
37
|
+
)
|
|
27
38
|
}
|
|
28
39
|
}
|
|
29
40
|
}
|
|
@@ -82,11 +82,11 @@ fun emitContextMenuItemPress(
|
|
|
82
82
|
|
|
83
83
|
fun parseMd4cFlags(flags: ReadableMap?): Md4cFlags =
|
|
84
84
|
Md4cFlags(
|
|
85
|
-
underline = flags
|
|
86
|
-
latexMath = FeatureFlags.IS_MATH_ENABLED &&
|
|
87
|
-
superscript = flags
|
|
88
|
-
subscript = flags
|
|
89
|
-
highlight = flags
|
|
85
|
+
underline = flags.getBooleanOrDefault("underline", false),
|
|
86
|
+
latexMath = FeatureFlags.IS_MATH_ENABLED && flags.getBooleanOrDefault("latexMath", true),
|
|
87
|
+
superscript = flags.getBooleanOrDefault("superscript", false),
|
|
88
|
+
subscript = flags.getBooleanOrDefault("subscript", false),
|
|
89
|
+
highlight = flags.getBooleanOrDefault("highlight", false),
|
|
90
90
|
)
|
|
91
91
|
|
|
92
92
|
fun parseContextMenuItems(value: ReadableArray?): List<String> =
|
|
@@ -95,8 +95,8 @@ fun parseContextMenuItems(value: ReadableArray?): List<String> =
|
|
|
95
95
|
fun parseSelectionMenuConfig(value: ReadableMap?): SelectionMenuConfig {
|
|
96
96
|
if (value == null) return SelectionMenuConfig()
|
|
97
97
|
return SelectionMenuConfig(
|
|
98
|
-
copyAsMarkdown = value.
|
|
99
|
-
copyImageUrl = value.
|
|
98
|
+
copyAsMarkdown = value.getBooleanOrDefault("copyAsMarkdown", true),
|
|
99
|
+
copyImageUrl = value.getBooleanOrDefault("copyImageUrl", true),
|
|
100
100
|
copyLabel = value.getString("copyLabel") ?: "",
|
|
101
101
|
copyAsMarkdownLabel = value.getString("copyAsMarkdownLabel") ?: "",
|
|
102
102
|
copyImageUrlLabel = value.getString("copyImageUrlLabel") ?: "",
|
package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/MarkdownStyleParser.kt
CHANGED
|
@@ -76,6 +76,8 @@ object MarkdownStyleParser {
|
|
|
76
76
|
if (mathMap?.hasKey("selectedBackgroundColor") == true) mathMap.getInt("selectedBackgroundColor") else null
|
|
77
77
|
val selectedBorderColor =
|
|
78
78
|
if (mathMap?.hasKey("selectedBorderColor") == true) mathMap.getInt("selectedBorderColor") else null
|
|
79
|
+
val fontWeight =
|
|
80
|
+
(if (mathMap?.hasKey("fontWeight") == true) mathMap.getString("fontWeight") else null) ?: "normal"
|
|
79
81
|
|
|
80
82
|
return MathChipStyle(
|
|
81
83
|
color = color,
|
|
@@ -88,6 +90,7 @@ object MarkdownStyleParser {
|
|
|
88
90
|
paddingVertical = paddingVertical,
|
|
89
91
|
selectedBackgroundColor = nullIfTransparent(selectedBackgroundColor),
|
|
90
92
|
selectedBorderColor = nullIfTransparent(selectedBorderColor),
|
|
93
|
+
fontWeight = fontWeight,
|
|
91
94
|
)
|
|
92
95
|
}
|
|
93
96
|
|
|
@@ -143,6 +146,12 @@ object MarkdownStyleParser {
|
|
|
143
146
|
} else {
|
|
144
147
|
inlineFallback.selectedBorderColor
|
|
145
148
|
}
|
|
149
|
+
val fontWeight =
|
|
150
|
+
if (mathBlockMap?.hasKey("fontWeight") == true) {
|
|
151
|
+
mathBlockMap.getString("fontWeight") ?: "normal"
|
|
152
|
+
} else {
|
|
153
|
+
inlineFallback.fontWeight
|
|
154
|
+
}
|
|
146
155
|
val marginTop =
|
|
147
156
|
PixelUtil.toPixelFromDIP(
|
|
148
157
|
if (mathBlockMap?.hasKey("marginTop") == true) mathBlockMap.getDouble("marginTop").toFloat() else 8f,
|
|
@@ -170,6 +179,7 @@ object MarkdownStyleParser {
|
|
|
170
179
|
paddingVertical = paddingVertical,
|
|
171
180
|
selectedBackgroundColor = nullIfTransparent(selectedBackgroundColor),
|
|
172
181
|
selectedBorderColor = nullIfTransparent(selectedBorderColor),
|
|
182
|
+
fontWeight = fontWeight,
|
|
173
183
|
marginTop = marginTop,
|
|
174
184
|
marginBottom = marginBottom,
|
|
175
185
|
alignment = alignment,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.utils.input
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The pure half of the paste interception path. A `ClipData` cannot be built on
|
|
5
|
+
* the JVM, so the joining rule lives here where a unit test can reach it; the
|
|
6
|
+
* two call sites (context menu and `onReceiveContent`) only extract the items.
|
|
7
|
+
*/
|
|
8
|
+
object PasteTextUtils {
|
|
9
|
+
/**
|
|
10
|
+
* Flattens a clip's items into one string. A clip regularly carries more than
|
|
11
|
+
* one item — reading only `getItemAt(0)` silently drops items 2..n. Items
|
|
12
|
+
* without text (a URI or intent) are skipped, and the rest join on newlines,
|
|
13
|
+
* which is what the platform's own paste does.
|
|
14
|
+
*/
|
|
15
|
+
fun joinClipTexts(texts: List<CharSequence?>): String =
|
|
16
|
+
texts
|
|
17
|
+
.mapNotNull { it?.toString() }
|
|
18
|
+
.filter { it.isNotEmpty() }
|
|
19
|
+
.joinToString("\n")
|
|
20
|
+
}
|
|
@@ -14,8 +14,16 @@ fun SpannableStringBuilder.isInlineImage(): Boolean {
|
|
|
14
14
|
return lastChar != '\n' && lastChar != '\u200B'
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Swaps MathInlineSpans for MathInlinePlaceholderSpans safe for background-thread measurement.
|
|
19
|
+
*
|
|
20
|
+
* [paddingVertical] must be the same `inlineMath.paddingVertical` (in px) the real spans were
|
|
21
|
+
* built with, or the measured line box won't match the drawn one.
|
|
22
|
+
*/
|
|
23
|
+
fun SpannableString.replaceMathSpansWithPlaceholders(
|
|
24
|
+
context: Context,
|
|
25
|
+
paddingVertical: Float,
|
|
26
|
+
) {
|
|
19
27
|
if (!FeatureFlags.IS_MATH_ENABLED) return
|
|
20
28
|
|
|
21
29
|
try {
|
|
@@ -42,7 +50,7 @@ fun SpannableString.replaceMathSpansWithPlaceholders(context: Context) {
|
|
|
42
50
|
@Suppress("UNCHECKED_CAST")
|
|
43
51
|
val results = measureMethod.invoke(null, context, requests) as? List<MathMetrics> ?: return
|
|
44
52
|
|
|
45
|
-
val placeholderCtor = placeholderClass.getConstructor(MathMetrics::class.java)
|
|
53
|
+
val placeholderCtor = placeholderClass.getConstructor(MathMetrics::class.java, Float::class.javaPrimitiveType)
|
|
46
54
|
|
|
47
55
|
mathSpans.forEachIndexed { i, oldSpan ->
|
|
48
56
|
val metrics = results.getOrNull(i) ?: return@forEachIndexed
|
|
@@ -53,7 +61,7 @@ fun SpannableString.replaceMathSpansWithPlaceholders(context: Context) {
|
|
|
53
61
|
|
|
54
62
|
if (start != -1 && end != -1) {
|
|
55
63
|
removeSpan(oldSpan)
|
|
56
|
-
val newSpan = placeholderCtor.newInstance(metrics)
|
|
64
|
+
val newSpan = placeholderCtor.newInstance(metrics, paddingVertical)
|
|
57
65
|
setSpan(newSpan, start, end, flags)
|
|
58
66
|
}
|
|
59
67
|
}
|
|
@@ -456,7 +456,7 @@ class TableContainerView(
|
|
|
456
456
|
.apply { configure(config, context) }
|
|
457
457
|
.renderDocument(MarkdownASTNode(NodeType.Document, children = listOf(paragraph)), null, null)
|
|
458
458
|
}
|
|
459
|
-
styledText.replaceMathSpansWithPlaceholders(context)
|
|
459
|
+
styledText.replaceMathSpansWithPlaceholders(context, config.inlineMathStyle.paddingVertical)
|
|
460
460
|
if (isHeader && styledText.isNotEmpty()) {
|
|
461
461
|
styledText.setSpan(HeaderTypefaceSpan(headerTypeface), 0, styledText.length, 33)
|
|
462
462
|
}
|
package/android/src/math/java/com/swmansion/enriched/markdown/renderer/MathInlineRenderer.kt
CHANGED
|
@@ -35,6 +35,8 @@ class MathInlineRenderer(
|
|
|
35
35
|
latex = latex,
|
|
36
36
|
fontSize = blockStyle.fontSize,
|
|
37
37
|
textColor = config.style.inlineMathStyle.color,
|
|
38
|
+
paddingVertical = config.style.inlineMathStyle.paddingVertical,
|
|
39
|
+
fontWeight = config.style.inlineMathStyle.fontWeight,
|
|
38
40
|
)
|
|
39
41
|
|
|
40
42
|
builder.setSpan(span, start, end, SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE)
|
|
@@ -66,7 +66,7 @@ class MathChipSpan(
|
|
|
66
66
|
displayMode = displayMode,
|
|
67
67
|
color = style.color,
|
|
68
68
|
)
|
|
69
|
-
val renderer = RaTeXRenderer(displayList, style.fontSize
|
|
69
|
+
val renderer = RaTeXRenderer(displayList, style.fontSize, ratexTypefaceLoader(style.fontWeight))
|
|
70
70
|
formulaWidth = renderer.widthPx
|
|
71
71
|
formulaAscent = renderer.heightPx
|
|
72
72
|
formulaDescent = renderer.depthPx
|
package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathInlinePlaceholderSpan.kt
CHANGED
|
@@ -3,11 +3,12 @@ package com.swmansion.enriched.markdown.spans
|
|
|
3
3
|
import android.graphics.Canvas
|
|
4
4
|
import android.graphics.Paint
|
|
5
5
|
import android.text.style.ReplacementSpan
|
|
6
|
-
import kotlin.math.
|
|
6
|
+
import kotlin.math.ceil
|
|
7
7
|
|
|
8
8
|
/** Holds pre-computed math metrics for background-thread measurement. */
|
|
9
9
|
class MathInlinePlaceholderSpan(
|
|
10
10
|
private val metrics: MathMetrics,
|
|
11
|
+
private val paddingVertical: Float,
|
|
11
12
|
) : ReplacementSpan() {
|
|
12
13
|
override fun getSize(
|
|
13
14
|
paint: Paint,
|
|
@@ -17,9 +18,12 @@ class MathInlinePlaceholderSpan(
|
|
|
17
18
|
fm: Paint.FontMetricsInt?,
|
|
18
19
|
): Int {
|
|
19
20
|
fm?.apply {
|
|
20
|
-
|
|
21
|
+
// Must stay identical to MathInlineSpan.getSize — this span stands in for it
|
|
22
|
+
// during ShadowNode measurement, so any divergence shows up as a clipped or
|
|
23
|
+
// over-tall view. Same ceil, same padding, same expression.
|
|
24
|
+
ascent = -ceil(metrics.ascent + paddingVertical).toInt()
|
|
21
25
|
top = ascent
|
|
22
|
-
descent = metrics.descent.
|
|
26
|
+
descent = ceil(metrics.descent + paddingVertical).toInt()
|
|
23
27
|
bottom = descent
|
|
24
28
|
}
|
|
25
29
|
return metrics.width
|
|
@@ -6,7 +6,6 @@ import android.graphics.Canvas
|
|
|
6
6
|
import android.graphics.Paint
|
|
7
7
|
import android.text.style.ReplacementSpan
|
|
8
8
|
import io.ratex.RaTeXEngine
|
|
9
|
-
import io.ratex.RaTeXFontLoader
|
|
10
9
|
import io.ratex.RaTeXRenderer
|
|
11
10
|
import kotlin.math.ceil
|
|
12
11
|
|
|
@@ -15,6 +14,8 @@ class MathInlineSpan(
|
|
|
15
14
|
internal val latex: String,
|
|
16
15
|
internal val fontSize: Float,
|
|
17
16
|
private val textColor: Int,
|
|
17
|
+
private val paddingVertical: Float,
|
|
18
|
+
private val fontWeight: String = "normal",
|
|
18
19
|
) : ReplacementSpan() {
|
|
19
20
|
private var cachedBitmap: Bitmap? = null
|
|
20
21
|
private var cachedWidth = 0
|
|
@@ -28,7 +29,7 @@ class MathInlineSpan(
|
|
|
28
29
|
|
|
29
30
|
try {
|
|
30
31
|
val displayList = RaTeXEngine.parseBlocking(latex, displayMode = false, color = textColor)
|
|
31
|
-
val renderer = RaTeXRenderer(displayList, fontSize
|
|
32
|
+
val renderer = RaTeXRenderer(displayList, fontSize, ratexTypefaceLoader(fontWeight))
|
|
32
33
|
|
|
33
34
|
cachedWidth = renderer.widthPx.toInt().coerceAtLeast(1)
|
|
34
35
|
mathAscent = renderer.heightPx
|
|
@@ -62,10 +63,12 @@ class MathInlineSpan(
|
|
|
62
63
|
prepareResources()
|
|
63
64
|
|
|
64
65
|
fm?.apply {
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
// Must stay identical to MathInlinePlaceholderSpan.getSize: that span measures
|
|
67
|
+
// this one off the main thread, and a box that disagrees by even a pixel either
|
|
68
|
+
// clips the formula or eats the padding this field is here to add.
|
|
69
|
+
ascent = -ceil(mathAscent + paddingVertical).toInt()
|
|
67
70
|
top = ascent
|
|
68
|
-
descent =
|
|
71
|
+
descent = ceil(mathDescent + paddingVertical).toInt()
|
|
69
72
|
bottom = descent
|
|
70
73
|
}
|
|
71
74
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.spans
|
|
2
|
+
|
|
3
|
+
import android.graphics.Typeface
|
|
4
|
+
import com.swmansion.enriched.markdown.styles.MathFontWeight
|
|
5
|
+
import io.ratex.RaTeXFontLoader
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Typeface supplier for [io.ratex.RaTeXRenderer]. RaTeX resolves every glyph to
|
|
9
|
+
* a KaTeX font id and asks us for the face to draw it with.
|
|
10
|
+
*
|
|
11
|
+
* Takes the raw `fontWeight` string the style model carries — the same shape
|
|
12
|
+
* every other `fontWeight` prop keeps — and is the single place on Android that
|
|
13
|
+
* narrows it to a weight we can draw. Consumers pass the style through; nothing
|
|
14
|
+
* upstream has to know that only two weights are reachable today.
|
|
15
|
+
*
|
|
16
|
+
* Bold is SYNTHETIC: asking for [Typeface.BOLD] on a single-weight asset makes
|
|
17
|
+
* the platform embolden it, which preserves the advances RaTeX already laid the
|
|
18
|
+
* glyphs out with. Substituting the bundled KaTeX bold faces instead would not
|
|
19
|
+
* — their advances run up to 15% wider than the regular ones the layout
|
|
20
|
+
* reserved, so glyphs would collide. iOS reaches the same result by stroking
|
|
21
|
+
* the glyph outline; see ENRMRaTeXBridge.
|
|
22
|
+
*/
|
|
23
|
+
internal fun ratexTypefaceLoader(fontWeight: String): (String) -> Typeface? =
|
|
24
|
+
when (MathFontWeight.fromString(fontWeight)) {
|
|
25
|
+
MathFontWeight.NORMAL -> { fontId -> RaTeXFontLoader.getTypeface(fontId) }
|
|
26
|
+
|
|
27
|
+
MathFontWeight.BOLD -> { fontId ->
|
|
28
|
+
RaTeXFontLoader.getTypeface(fontId)?.let { Typeface.create(it, Typeface.BOLD) }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -15,6 +15,7 @@ import com.swmansion.enriched.markdown.accessibility.AccessibilityLabels
|
|
|
15
15
|
import com.swmansion.enriched.markdown.spans.MathMeasureHelper
|
|
16
16
|
import com.swmansion.enriched.markdown.spans.MathMeasureRequest
|
|
17
17
|
import com.swmansion.enriched.markdown.spans.MathRenderMode
|
|
18
|
+
import com.swmansion.enriched.markdown.spans.ratexTypefaceLoader
|
|
18
19
|
import com.swmansion.enriched.markdown.styles.MathStyle
|
|
19
20
|
import com.swmansion.enriched.markdown.styles.StyleConfig
|
|
20
21
|
import io.ratex.RaTeXEngine
|
|
@@ -100,7 +101,8 @@ class MathContainerView(
|
|
|
100
101
|
cachedLatex = latex
|
|
101
102
|
try {
|
|
102
103
|
val displayList = RaTeXEngine.parseBlocking(latex, displayMode = true, color = mathStyle.color)
|
|
103
|
-
mathView.renderer =
|
|
104
|
+
mathView.renderer =
|
|
105
|
+
RaTeXRenderer(displayList, mathStyle.fontSize, ratexTypefaceLoader(mathStyle.fontWeight))
|
|
104
106
|
} catch (e: Exception) {
|
|
105
107
|
Log.e(TAG, "Failed to render LaTeX", e)
|
|
106
108
|
mathView.renderer = null
|
package/docs/API_REFERENCE.md
CHANGED
|
@@ -515,6 +515,15 @@ Whether the input is scrollable when content exceeds the visible area.
|
|
|
515
515
|
| --------- | ------------- | -------- |
|
|
516
516
|
| `boolean` | `true` | Both |
|
|
517
517
|
|
|
518
|
+
### `blockSyntax`
|
|
519
|
+
|
|
520
|
+
Whether block-level Markdown syntax is interpreted. See
|
|
521
|
+
[Preserving block syntax](./INPUT.md#preserving-block-syntax).
|
|
522
|
+
|
|
523
|
+
| Type | Default Value | Platform |
|
|
524
|
+
| --------- | ------------- | -------- |
|
|
525
|
+
| `boolean` | `true` | Both |
|
|
526
|
+
|
|
518
527
|
### `autoCapitalize`
|
|
519
528
|
|
|
520
529
|
Auto-capitalization behavior.
|
|
@@ -523,6 +532,18 @@ Auto-capitalization behavior.
|
|
|
523
532
|
| -------- | -------------- | -------- |
|
|
524
533
|
| `string` | `'sentences'` | Both |
|
|
525
534
|
|
|
535
|
+
### `interceptPaste`
|
|
536
|
+
|
|
537
|
+
Reports pastes through [`onPaste`](#onpaste) instead of inserting them. Only takes effect when `onPaste` is also set — otherwise native would swallow every paste with nobody left to resolve it, so the library keeps the prop off.
|
|
538
|
+
|
|
539
|
+
See [Intercepting paste](./INPUT.md#intercepting-paste).
|
|
540
|
+
|
|
541
|
+
| Type | Default Value | Platform |
|
|
542
|
+
| --------- | ------------- | ------------- |
|
|
543
|
+
| `boolean` | `false` | iOS, Android |
|
|
544
|
+
|
|
545
|
+
macOS ignores this prop and always pastes inline.
|
|
546
|
+
|
|
526
547
|
### `multiline`
|
|
527
548
|
|
|
528
549
|
Whether the input supports multiple lines.
|
|
@@ -701,6 +722,18 @@ Fires when the active mention flow ends.
|
|
|
701
722
|
| ---- | ------------- | -------- |
|
|
702
723
|
| `(event: { indicator: string }) => void` | - | Both |
|
|
703
724
|
|
|
725
|
+
### `onPaste`
|
|
726
|
+
|
|
727
|
+
Fires instead of the insert when [`interceptPaste`](#interceptpaste) is on. Native reads the clipboard and leaves the buffer untouched, so the handler owns the paste from that point: it must call [`resolvePaste`](#resolvepastetext-string-markdown-string) exactly once, on every path including failures, or the paste is silently dropped.
|
|
728
|
+
|
|
729
|
+
| Type | Default Value | Platform |
|
|
730
|
+
| ---- | ------------- | -------- |
|
|
731
|
+
| `(event: { text: string; markdown: string }) => void` | - | iOS, Android |
|
|
732
|
+
|
|
733
|
+
`markdown` is non-empty only when the copy source was an `EnrichedMarkdownTextInput`, which the system clipboard can only carry on iOS; Android always reports `''`.
|
|
734
|
+
|
|
735
|
+
See [Intercepting paste](./INPUT.md#intercepting-paste).
|
|
736
|
+
|
|
704
737
|
### `writingDirection`
|
|
705
738
|
|
|
706
739
|
Paragraph writing direction in the input. iOS only — Android's `EditText` already resolves direction per paragraph via `TEXT_DIRECTION_FIRST_STRONG` and is unaffected by this prop.
|
|
@@ -933,6 +966,16 @@ Copies the input's full content to the system clipboard, matching the result of
|
|
|
933
966
|
|
|
934
967
|
On iOS and macOS the clipboard receives both plain text and a private Markdown pasteboard type, so pasting back into an `EnrichedMarkdownTextInput` restores the formatting; external apps receive plain text only. On Android the clipboard receives plain text only — inline styles are not preserved for any paste target.
|
|
935
968
|
|
|
969
|
+
### `resolvePaste(text: string, markdown?: string)`
|
|
970
|
+
|
|
971
|
+
Completes a paste reported through [`onPaste`](#onpaste). Call it exactly once per event:
|
|
972
|
+
|
|
973
|
+
- `resolvePaste(text, markdown)` — inserts, restoring the copied formatting when `markdown` is non-empty.
|
|
974
|
+
- `resolvePaste(text)` — inserts `text` verbatim.
|
|
975
|
+
- `resolvePaste('', '')` — drops the clipboard content, replacing the current selection. With an empty selection this is a no-op that still emits `onChangeText` with unchanged content.
|
|
976
|
+
|
|
977
|
+
Anything that replaces a selection replaces it here too, so a paste over selected text behaves the same on every branch.
|
|
978
|
+
|
|
936
979
|
### `startMention(indicator: string)`
|
|
937
980
|
|
|
938
981
|
Programmatically triggers a mention flow by inserting the indicator character at the current cursor position. The indicator must be listed in the `mentionIndicators` prop. Useful for toolbar buttons.
|
package/docs/INPUT.md
CHANGED
|
@@ -122,6 +122,50 @@ The callback fires only for newly detected links — not for links that were alr
|
|
|
122
122
|
|
|
123
123
|
When a manual link is applied (via `setLink` or `insertLink`) over an auto-detected link, the auto-detected link is replaced by the manual one. Auto-link detection skips ranges that already contain a manual link.
|
|
124
124
|
|
|
125
|
+
## Preserving block syntax
|
|
126
|
+
|
|
127
|
+
By default the input interprets block-level Markdown: a line starting with `1) `, `- `,
|
|
128
|
+
`# ` or `> ` is consumed as list, heading or quote *structure*, and the marker itself is
|
|
129
|
+
not part of the text. The editor has no block styles to render that structure with and
|
|
130
|
+
the serializer has no way to write the marker back, so the marker is lost on save. Two
|
|
131
|
+
adjacent list items can end up with their formulas touching — `1) $a+b$` and `2) $c+d$`
|
|
132
|
+
save as `$a+b$$c+d$`, where the `$$` now reads as display math.
|
|
133
|
+
|
|
134
|
+
Set `blockSyntax={false}` when the text must survive the round trip unchanged:
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
<EnrichedMarkdownTextInput
|
|
138
|
+
blockSyntax={false}
|
|
139
|
+
defaultValue={markdown}
|
|
140
|
+
onChangeMarkdown={setMarkdown}
|
|
141
|
+
/>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
In this mode the parser never sees block structure, so markers stay in the text verbatim
|
|
145
|
+
and `onChangeMarkdown` returns what was loaded, byte for byte. Inline syntax is unaffected:
|
|
146
|
+
`**bold**`, `*italic*`, `~~strike~~`, `||spoiler||`, links and `$math$` all still parse and
|
|
147
|
+
style as usual.
|
|
148
|
+
|
|
149
|
+
What changes:
|
|
150
|
+
|
|
151
|
+
- **Typing `3) ` no longer starts a list**, and `# ` no longer makes a heading. Nothing
|
|
152
|
+
block-level is interpreted while typing either — that is the point of the mode.
|
|
153
|
+
- The text is displayed with its markers showing, so an editor with `blockSyntax={false}`
|
|
154
|
+
and a `EnrichedMarkdownText` rendering the same source look different, even though the
|
|
155
|
+
stored string is identical.
|
|
156
|
+
- `\r\n` and `\r` are normalized to `\n`, and `U+000B` (vertical tab) is stripped from the
|
|
157
|
+
input. `U+000B` is used internally to hide line structure from the Markdown parser, so it
|
|
158
|
+
cannot be carried through as content.
|
|
159
|
+
|
|
160
|
+
Three inline constructs are still lossy in this mode, exactly as they are with
|
|
161
|
+
`blockSyntax={true}` — they are not regressions introduced by the flag:
|
|
162
|
+
|
|
163
|
+
| Input | Saved as | Why |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| `` gõ `x` vào `` | `gõ x vào` | The editor has no inline code style, so the backticks have nothing to round-trip through |
|
|
166
|
+
| `Giá 5\$` | `Giá 5$` | The serializer does not re-escape `\$`, which can leave an unpaired `$` |
|
|
167
|
+
| `A &amp; B` | `A B` | HTML entity nodes are dropped. A raw `&` is safe |
|
|
168
|
+
|
|
125
169
|
## Caret Position Tracking
|
|
126
170
|
|
|
127
171
|
`EnrichedMarkdownTextInput` can report the caret's pixel position relative to the input, which is useful when the input is embedded in a scrollable container with `scrollEnabled={false}` and you need to keep the caret visible.
|
|
@@ -161,6 +205,45 @@ The input's content can be copied to the system clipboard from a ref, without re
|
|
|
161
205
|
|
|
162
206
|
On iOS and macOS the clipboard receives both plain text and a private Markdown pasteboard type, so pasting back into an `EnrichedMarkdownTextInput` restores the formatting; external apps receive plain text only. On Android the clipboard receives plain text only — inline styles are not preserved for any paste target.
|
|
163
207
|
|
|
208
|
+
## Intercepting paste
|
|
209
|
+
|
|
210
|
+
By default a paste goes straight into the buffer. Set [`interceptPaste`](API_REFERENCE.md#interceptpaste) together with [`onPaste`](API_REFERENCE.md#onpaste) to decide what a paste means before any text lands — turning a long paste into an attachment chip, stripping it, or inserting it unchanged.
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
const inputRef = useRef<EnrichedMarkdownTextInputInstance>(null);
|
|
214
|
+
|
|
215
|
+
<EnrichedMarkdownTextInput
|
|
216
|
+
ref={inputRef}
|
|
217
|
+
interceptPaste
|
|
218
|
+
onPaste={({ text, markdown }) => {
|
|
219
|
+
if (text.length > 2000) {
|
|
220
|
+
attachAsFile(text);
|
|
221
|
+
inputRef.current?.resolvePaste('', ''); // drops it, and replaces the selection
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
inputRef.current?.resolvePaste(text, markdown);
|
|
225
|
+
}}
|
|
226
|
+
/>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Native reads the clipboard and **does not touch the buffer**. That makes the handler the only thing that can complete the paste, so the contract is strict:
|
|
230
|
+
|
|
231
|
+
- Call [`resolvePaste`](API_REFERENCE.md#resolvepastetext-string-markdown-string) **exactly once per event, on every path including failures**. A handler that throws before resolving leaves the paste dropped — the user sees nothing happen.
|
|
232
|
+
- Pass `markdown` back through when inserting. It is the copied formatting; dropping it turns a copy-then-paste inside your app into flat text.
|
|
233
|
+
- `interceptPaste` is ignored unless `onPaste` is set, so switching the prop on alone cannot make the input swallow pastes.
|
|
234
|
+
|
|
235
|
+
Because nothing is written until you resolve, an async decision is safe: the selection is still there, and the user sees an unchanged input rather than text that vanished.
|
|
236
|
+
|
|
237
|
+
### Platform behavior
|
|
238
|
+
|
|
239
|
+
| | Behavior |
|
|
240
|
+
| --- | --- |
|
|
241
|
+
| **iOS** | Context menu and hardware ⌘V. `markdown` is set when the copy source was an `EnrichedMarkdownTextInput`. |
|
|
242
|
+
| **Android** | Context menu, hardware Ctrl+V and `KEYCODE_PASTE`. `markdown` is always `''` — the Android clipboard carries plain text only. |
|
|
243
|
+
| **macOS** | `interceptPaste` is ignored, `onPaste` never fires, and pastes insert inline as usual. |
|
|
244
|
+
|
|
245
|
+
Only clipboard content is intercepted. Drag-and-drop, autofill and IME content insertion bypass `onPaste` on both platforms and insert normally — on Android they are filtered out explicitly, so an autofill never arrives looking like a paste.
|
|
246
|
+
|
|
164
247
|
## Style Detection
|
|
165
248
|
|
|
166
249
|
All of the above styles can be detected with the use of [onChangeState](API_REFERENCE.md#onchangestate) callback payload.
|
|
@@ -231,3 +314,7 @@ Available style properties:
|
|
|
231
314
|
- `link.underline` — whether links are underlined (defaults to `true`).
|
|
232
315
|
- `spoiler.color` — text color for spoiler text.
|
|
233
316
|
- `spoiler.backgroundColor` — background color for spoiler text.
|
|
317
|
+
- `math.fontWeight` — `'bold'` | `'normal'` (default `'normal'`); typesets inline formula chips
|
|
318
|
+
(`$...$`) at bold weight. See [LATEX_MATH.md](./LATEX_MATH.md#bold-formulas).
|
|
319
|
+
- `mathBlock.fontWeight` — same for block formula chips (`$$...$$`); inherits from
|
|
320
|
+
`math.fontWeight` when unset.
|
package/docs/LATEX_MATH.md
CHANGED
|
@@ -27,6 +27,7 @@ Einstein's mass-energy equivalence $E = mc^2$ is one of the most famous equation
|
|
|
27
27
|
},
|
|
28
28
|
inlineMath: {
|
|
29
29
|
color: '#1F2937',
|
|
30
|
+
paddingVertical: 4,
|
|
30
31
|
},
|
|
31
32
|
}}
|
|
32
33
|
/>
|
|
@@ -34,6 +35,62 @@ Einstein's mass-energy equivalence $E = mc^2$ is one of the most famous equation
|
|
|
34
35
|
|
|
35
36
|
Block math equations are rendered as standalone display elements with spacing and an optional background. Inline math inherits the surrounding block's typography.
|
|
36
37
|
|
|
38
|
+
### Spacing around inline formulas
|
|
39
|
+
|
|
40
|
+
An inline formula reports its own height, and tall constructs such as `\frac` are routinely taller than the
|
|
41
|
+
surrounding text. The block's `lineHeight` cannot loosen those lines: it is applied expand-only, so a value
|
|
42
|
+
below the formula's natural height is ignored rather than compressing the formula out of its box. Two wrapped
|
|
43
|
+
lines that each contain a fraction therefore sit flush against each other.
|
|
44
|
+
|
|
45
|
+
`inlineMath.paddingVertical` (points, default `4`) adds empty space above and below the formula box, which is
|
|
46
|
+
what pushes those lines apart. It affects only lines that actually contain inline math — prose-only lines keep
|
|
47
|
+
the line height they had. Set it to `0` to get the old flush behaviour back. Block math (`$$...$$`) has its own
|
|
48
|
+
`math.padding` instead.
|
|
49
|
+
|
|
50
|
+
### Bold formulas
|
|
51
|
+
|
|
52
|
+
`math.fontWeight` / `inlineMath.fontWeight` (`'normal'` | `'bold'`, default `'normal'`) typeset a
|
|
53
|
+
formula at bold weight. Set it wherever the surrounding text is bold so the formula follows it.
|
|
54
|
+
|
|
55
|
+
The embolden is **synthetic on every platform** — no real bold face is ever used.
|
|
56
|
+
|
|
57
|
+
On **iOS and Android** it is a stroke on the glyph outline and the platform's fake-bold
|
|
58
|
+
respectively, rather than a switch to the bundled KaTeX bold faces. That is deliberate. The native
|
|
59
|
+
engine has no weight parameter, and by the time a style applies, glyph positions are already laid
|
|
60
|
+
out: the bold faces' advances run up to 15% wider than the regular ones the layout reserved, so
|
|
61
|
+
swapping them in would make glyphs collide. Stroking and fake-bold both preserve every advance.
|
|
62
|
+
|
|
63
|
+
Two consequences worth knowing:
|
|
64
|
+
|
|
65
|
+
- Fraction bars and rules keep their own thickness, so they do not thicken with the glyphs.
|
|
66
|
+
- On iOS the stroke width also applies to the handful of decorations the engine strokes itself
|
|
67
|
+
(`\cancel`, `\textcircled`, `\corresponds`, `\imageof`). Prefer turning bold on per surface
|
|
68
|
+
rather than globally.
|
|
69
|
+
|
|
70
|
+
On **web** the prop is a plain CSS declaration on the KaTeX output — but the library renders in
|
|
71
|
+
[MathML output mode](#web) and loads no KaTeX stylesheet or fonts, so `font-weight` is resolved by
|
|
72
|
+
the browser's own math font (STIX Two Math, Latin Modern Math, Cambria Math). Most of those ship no
|
|
73
|
+
bold face, so the browser synthesizes one there too. Treat web as synthetic as well, not as a
|
|
74
|
+
reference for how heavy bold "should" look.
|
|
75
|
+
|
|
76
|
+
`EnrichedMarkdownTextInput` honours the same prop for the formula chips typed into the editor, under
|
|
77
|
+
its own style keys — `math.fontWeight` for inline chips (`$...$`) and `mathBlock.fontWeight` for
|
|
78
|
+
block chips (`$$...$$`). `mathBlock` inherits the weight from `math` when it does not set one of its
|
|
79
|
+
own, the way it already inherits `color` and the border props:
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
<EnrichedMarkdownTextInput
|
|
83
|
+
markdownStyle={{
|
|
84
|
+
math: { fontWeight: 'bold' }, // inline chips bold…
|
|
85
|
+
mathBlock: { fontWeight: 'normal' }, // …block chips opt back out
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The caveats above apply unchanged — the embolden is the same synthetic one, and the chip keeps its
|
|
91
|
+
measured width because fake-bold and stroking both preserve the glyph advances. Editor chips are
|
|
92
|
+
iOS + Android only; there is no web or macOS chip surface.
|
|
93
|
+
|
|
37
94
|
> [!IMPORTANT]
|
|
38
95
|
> LaTeX commands use backslashes (e.g. `\frac`, `\alpha`). In regular JS strings and template literals, backslashes are escape characters. Use `String.raw` or double backslashes (`\\frac`) to preserve them. Block math (`$$...$$`) must be on its own line to render as a display element.
|
|
39
96
|
|
|
@@ -53,6 +110,11 @@ yarn add katex
|
|
|
53
110
|
|
|
54
111
|
KaTeX is loaded lazily the first time a LaTeX node is encountered, so it has no impact on pages that do not render math. No stylesheet or `<link>` tag is needed.
|
|
55
112
|
|
|
113
|
+
If you load `katex/dist/katex.min.css` anyway — some setups do — its `.katex` rule opens with the
|
|
114
|
+
`font` shorthand, which resets `font-weight` and would drop [`fontWeight`](#bold-formulas) on the
|
|
115
|
+
way down to the formula. The library injects `.katex { font-weight: inherit; }` to keep the prop
|
|
116
|
+
working in that case; nothing is required of you.
|
|
117
|
+
|
|
56
118
|
> [!NOTE]
|
|
57
119
|
> MathML is supported natively in Chrome 109+, Firefox, and Safari. Older browsers will display the raw LaTeX source as a text fallback.
|
|
58
120
|
|