react-native-enriched-markdown-scaffold 1.0.2-scaffold.7 → 1.0.2-scaffold.9
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 +2 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +24 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +22 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +22 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +46 -8
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineMathStyle.kt +16 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/MathStyle.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/HeadlessActionMode.kt +88 -0
- 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 +1 -0
- package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathInlineSpan.kt +13 -6
- package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathMeasureHelper.kt +2 -1
- package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +11 -4
- package/ios/attachments/ENRMMathInlineAttachment+macOS.m +2 -1
- package/ios/attachments/ENRMMathInlineAttachment.h +3 -0
- package/ios/attachments/ENRMMathInlineAttachment.m +2 -1
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +24 -0
- package/ios/internals/MeasurementCache.h +3 -2
- package/ios/math/ENRMRaTeXBridge.swift +23 -5
- package/ios/renderer/ENRMMathInlineRenderer.m +1 -0
- package/ios/styles/StyleConfig.h +4 -0
- package/ios/styles/StyleConfig.mm +24 -0
- package/ios/utils/StylePropsUtils.h +10 -0
- package/ios/views/ENRMMathContainerView.m +4 -2
- package/lib/module/EnrichedMarkdownNativeComponent.ts +2 -0
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +2 -0
- package/lib/module/normalizeMarkdownStyle.js +4 -2
- package/lib/module/normalizeMarkdownStyle.js.map +1 -1
- package/lib/module/normalizeMarkdownStyle.web.js +4 -2
- package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +2 -0
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +2 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.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/types/MarkdownStyle.d.ts +22 -0
- package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +2 -0
- package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownTextProps.d.ts +4 -0
- package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/EnrichedMarkdownNativeComponent.ts +2 -0
- package/src/EnrichedMarkdownTextNativeComponent.ts +2 -0
- package/src/normalizeMarkdownStyle.ts +2 -0
- package/src/normalizeMarkdownStyle.web.ts +2 -0
- package/src/types/MarkdownStyle.ts +22 -0
- package/src/types/MarkdownStyleInternal.ts +2 -0
- package/src/types/MarkdownTextProps.ts +4 -0
package/android/build.gradle
CHANGED
|
@@ -270,6 +270,8 @@ dependencies {
|
|
|
270
270
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
271
271
|
// Baseline Profile installer for AOT compilation hints
|
|
272
272
|
implementation "androidx.profileinstaller:profileinstaller:1.3.1"
|
|
273
|
+
// AppCompatTextView draws emoji through EmojiCompat; measurement must apply it too
|
|
274
|
+
implementation "androidx.emoji2:emoji2:1.4.0"
|
|
273
275
|
// LaTeX math rendering (optional — set enrichedMarkdown.enableMath=false in gradle.properties to exclude)
|
|
274
276
|
if (enableMath) {
|
|
275
277
|
implementation("io.github.erweixin:ratex-android:0.1.10")
|
|
@@ -1761,6 +1761,7 @@ struct EnrichedMarkdownMarkdownStyleMathStruct {
|
|
|
1761
1761
|
Float marginBottom{0.0};
|
|
1762
1762
|
std::string textAlign{};
|
|
1763
1763
|
std::string fontWeight{};
|
|
1764
|
+
Float glyphOverflow{0.0};
|
|
1764
1765
|
|
|
1765
1766
|
|
|
1766
1767
|
#ifdef RN_SERIALIZABLE_STATE
|
|
@@ -1776,6 +1777,7 @@ struct EnrichedMarkdownMarkdownStyleMathStruct {
|
|
|
1776
1777
|
result["marginBottom"] = marginBottom;
|
|
1777
1778
|
result["textAlign"] = textAlign;
|
|
1778
1779
|
result["fontWeight"] = fontWeight;
|
|
1780
|
+
result["glyphOverflow"] = glyphOverflow;
|
|
1779
1781
|
return result;
|
|
1780
1782
|
}
|
|
1781
1783
|
#endif
|
|
@@ -1816,6 +1818,10 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
|
|
|
1816
1818
|
if (tmp_fontWeight != map.end()) {
|
|
1817
1819
|
fromRawValue(context, tmp_fontWeight->second, result.fontWeight);
|
|
1818
1820
|
}
|
|
1821
|
+
auto tmp_glyphOverflow = map.find("glyphOverflow");
|
|
1822
|
+
if (tmp_glyphOverflow != map.end()) {
|
|
1823
|
+
fromRawValue(context, tmp_glyphOverflow->second, result.glyphOverflow);
|
|
1824
|
+
}
|
|
1819
1825
|
}
|
|
1820
1826
|
|
|
1821
1827
|
static inline std::string toString(const EnrichedMarkdownMarkdownStyleMathStruct &value) {
|
|
@@ -1832,6 +1838,7 @@ struct EnrichedMarkdownMarkdownStyleInlineMathStruct {
|
|
|
1832
1838
|
SharedColor color{};
|
|
1833
1839
|
Float paddingVertical{0.0};
|
|
1834
1840
|
std::string fontWeight{};
|
|
1841
|
+
Float glyphOverflow{0.0};
|
|
1835
1842
|
|
|
1836
1843
|
|
|
1837
1844
|
#ifdef RN_SERIALIZABLE_STATE
|
|
@@ -1842,6 +1849,7 @@ struct EnrichedMarkdownMarkdownStyleInlineMathStruct {
|
|
|
1842
1849
|
result["color"] = ::facebook::react::toDynamic(color);
|
|
1843
1850
|
result["paddingVertical"] = paddingVertical;
|
|
1844
1851
|
result["fontWeight"] = fontWeight;
|
|
1852
|
+
result["glyphOverflow"] = glyphOverflow;
|
|
1845
1853
|
return result;
|
|
1846
1854
|
}
|
|
1847
1855
|
#endif
|
|
@@ -1862,6 +1870,10 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
|
|
|
1862
1870
|
if (tmp_fontWeight != map.end()) {
|
|
1863
1871
|
fromRawValue(context, tmp_fontWeight->second, result.fontWeight);
|
|
1864
1872
|
}
|
|
1873
|
+
auto tmp_glyphOverflow = map.find("glyphOverflow");
|
|
1874
|
+
if (tmp_glyphOverflow != map.end()) {
|
|
1875
|
+
fromRawValue(context, tmp_glyphOverflow->second, result.glyphOverflow);
|
|
1876
|
+
}
|
|
1865
1877
|
}
|
|
1866
1878
|
|
|
1867
1879
|
static inline std::string toString(const EnrichedMarkdownMarkdownStyleInlineMathStruct &value) {
|
|
@@ -5807,6 +5819,7 @@ struct EnrichedMarkdownTextMarkdownStyleMathStruct {
|
|
|
5807
5819
|
Float marginBottom{0.0};
|
|
5808
5820
|
std::string textAlign{};
|
|
5809
5821
|
std::string fontWeight{};
|
|
5822
|
+
Float glyphOverflow{0.0};
|
|
5810
5823
|
|
|
5811
5824
|
|
|
5812
5825
|
#ifdef RN_SERIALIZABLE_STATE
|
|
@@ -5822,6 +5835,7 @@ struct EnrichedMarkdownTextMarkdownStyleMathStruct {
|
|
|
5822
5835
|
result["marginBottom"] = marginBottom;
|
|
5823
5836
|
result["textAlign"] = textAlign;
|
|
5824
5837
|
result["fontWeight"] = fontWeight;
|
|
5838
|
+
result["glyphOverflow"] = glyphOverflow;
|
|
5825
5839
|
return result;
|
|
5826
5840
|
}
|
|
5827
5841
|
#endif
|
|
@@ -5862,6 +5876,10 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
|
|
|
5862
5876
|
if (tmp_fontWeight != map.end()) {
|
|
5863
5877
|
fromRawValue(context, tmp_fontWeight->second, result.fontWeight);
|
|
5864
5878
|
}
|
|
5879
|
+
auto tmp_glyphOverflow = map.find("glyphOverflow");
|
|
5880
|
+
if (tmp_glyphOverflow != map.end()) {
|
|
5881
|
+
fromRawValue(context, tmp_glyphOverflow->second, result.glyphOverflow);
|
|
5882
|
+
}
|
|
5865
5883
|
}
|
|
5866
5884
|
|
|
5867
5885
|
static inline std::string toString(const EnrichedMarkdownTextMarkdownStyleMathStruct &value) {
|
|
@@ -5878,6 +5896,7 @@ struct EnrichedMarkdownTextMarkdownStyleInlineMathStruct {
|
|
|
5878
5896
|
SharedColor color{};
|
|
5879
5897
|
Float paddingVertical{0.0};
|
|
5880
5898
|
std::string fontWeight{};
|
|
5899
|
+
Float glyphOverflow{0.0};
|
|
5881
5900
|
|
|
5882
5901
|
|
|
5883
5902
|
#ifdef RN_SERIALIZABLE_STATE
|
|
@@ -5888,6 +5907,7 @@ struct EnrichedMarkdownTextMarkdownStyleInlineMathStruct {
|
|
|
5888
5907
|
result["color"] = ::facebook::react::toDynamic(color);
|
|
5889
5908
|
result["paddingVertical"] = paddingVertical;
|
|
5890
5909
|
result["fontWeight"] = fontWeight;
|
|
5910
|
+
result["glyphOverflow"] = glyphOverflow;
|
|
5891
5911
|
return result;
|
|
5892
5912
|
}
|
|
5893
5913
|
#endif
|
|
@@ -5908,6 +5928,10 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
|
|
|
5908
5928
|
if (tmp_fontWeight != map.end()) {
|
|
5909
5929
|
fromRawValue(context, tmp_fontWeight->second, result.fontWeight);
|
|
5910
5930
|
}
|
|
5931
|
+
auto tmp_glyphOverflow = map.find("glyphOverflow");
|
|
5932
|
+
if (tmp_glyphOverflow != map.end()) {
|
|
5933
|
+
fromRawValue(context, tmp_glyphOverflow->second, result.glyphOverflow);
|
|
5934
|
+
}
|
|
5911
5935
|
}
|
|
5912
5936
|
|
|
5913
5937
|
static inline std::string toString(const EnrichedMarkdownTextMarkdownStyleInlineMathStruct &value) {
|
package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt
CHANGED
|
@@ -13,6 +13,7 @@ import com.swmansion.enriched.markdown.spoiler.SpoilerCapable
|
|
|
13
13
|
import com.swmansion.enriched.markdown.spoiler.SpoilerOverlay
|
|
14
14
|
import com.swmansion.enriched.markdown.spoiler.SpoilerOverlayDrawer
|
|
15
15
|
import com.swmansion.enriched.markdown.utils.text.interaction.CheckboxTouchHelper
|
|
16
|
+
import com.swmansion.enriched.markdown.utils.text.view.HeadlessActionMode
|
|
16
17
|
import com.swmansion.enriched.markdown.utils.text.view.LinkLongPressMovementMethod
|
|
17
18
|
import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig
|
|
18
19
|
import com.swmansion.enriched.markdown.utils.text.view.applySelectableState
|
|
@@ -20,6 +21,7 @@ import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForCheckboxT
|
|
|
20
21
|
import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForLinkTap
|
|
21
22
|
import com.swmansion.enriched.markdown.utils.text.view.clearTextSelection
|
|
22
23
|
import com.swmansion.enriched.markdown.utils.text.view.createSelectionActionModeCallback
|
|
24
|
+
import com.swmansion.enriched.markdown.utils.text.view.isRomSelectionPopupShower
|
|
23
25
|
import com.swmansion.enriched.markdown.utils.text.view.reallowParentInterceptIfLinkReleased
|
|
24
26
|
import com.swmansion.enriched.markdown.utils.text.view.setupAsMarkdownTextView
|
|
25
27
|
import com.swmansion.enriched.markdown.views.BlockSegmentView
|
|
@@ -128,6 +130,26 @@ class EnrichedMarkdownInternalText
|
|
|
128
130
|
onSelectionChangedCallback?.invoke(this, selEnd > selStart)
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
// See HeadlessActionMode: with the menu hidden, no toolbar window is created.
|
|
134
|
+
override fun startActionMode(
|
|
135
|
+
callback: ActionMode.Callback?,
|
|
136
|
+
type: Int,
|
|
137
|
+
): ActionMode? {
|
|
138
|
+
if (contextMenuHidden && type == ActionMode.TYPE_FLOATING && callback != null) {
|
|
139
|
+
return HeadlessActionMode.start(this, callback)
|
|
140
|
+
}
|
|
141
|
+
return super.startActionMode(callback, type)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// See isRomSelectionPopupShower: MIUI shows its own selection popup this way.
|
|
145
|
+
override fun postDelayed(
|
|
146
|
+
action: Runnable?,
|
|
147
|
+
delayMillis: Long,
|
|
148
|
+
): Boolean {
|
|
149
|
+
if (contextMenuHidden && isRomSelectionPopupShower(action)) return false
|
|
150
|
+
return super.postDelayed(action, delayMillis)
|
|
151
|
+
}
|
|
152
|
+
|
|
131
153
|
/** Clears the selection and ends its ActionMode. */
|
|
132
154
|
fun clearSelection() {
|
|
133
155
|
clearTextSelection()
|
|
@@ -28,6 +28,7 @@ import com.swmansion.enriched.markdown.utils.common.emitLinkLayout
|
|
|
28
28
|
import com.swmansion.enriched.markdown.utils.common.emitSelectionChange
|
|
29
29
|
import com.swmansion.enriched.markdown.utils.text.TailFadeInAnimator
|
|
30
30
|
import com.swmansion.enriched.markdown.utils.text.interaction.CheckboxTouchHelper
|
|
31
|
+
import com.swmansion.enriched.markdown.utils.text.view.HeadlessActionMode
|
|
31
32
|
import com.swmansion.enriched.markdown.utils.text.view.LinkLayoutCollector
|
|
32
33
|
import com.swmansion.enriched.markdown.utils.text.view.LinkLayoutFragment
|
|
33
34
|
import com.swmansion.enriched.markdown.utils.text.view.LinkLongPressMovementMethod
|
|
@@ -43,6 +44,7 @@ import com.swmansion.enriched.markdown.utils.text.view.createSelectionActionMode
|
|
|
43
44
|
import com.swmansion.enriched.markdown.utils.text.view.currentTextSelection
|
|
44
45
|
import com.swmansion.enriched.markdown.utils.text.view.emitLinkLongPressEvent
|
|
45
46
|
import com.swmansion.enriched.markdown.utils.text.view.emitLinkPressEvent
|
|
47
|
+
import com.swmansion.enriched.markdown.utils.text.view.isRomSelectionPopupShower
|
|
46
48
|
import com.swmansion.enriched.markdown.utils.text.view.reallowParentInterceptIfLinkReleased
|
|
47
49
|
import com.swmansion.enriched.markdown.utils.text.view.setupAsMarkdownTextView
|
|
48
50
|
import java.util.concurrent.Executors
|
|
@@ -336,6 +338,26 @@ class EnrichedMarkdownText
|
|
|
336
338
|
selectionReporter?.enabled = enabled
|
|
337
339
|
}
|
|
338
340
|
|
|
341
|
+
// See HeadlessActionMode: with the menu hidden, no toolbar window is created.
|
|
342
|
+
override fun startActionMode(
|
|
343
|
+
callback: ActionMode.Callback?,
|
|
344
|
+
type: Int,
|
|
345
|
+
): ActionMode? {
|
|
346
|
+
if (contextMenuHidden && type == ActionMode.TYPE_FLOATING && callback != null) {
|
|
347
|
+
return HeadlessActionMode.start(this, callback)
|
|
348
|
+
}
|
|
349
|
+
return super.startActionMode(callback, type)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// See isRomSelectionPopupShower: MIUI shows its own selection popup this way.
|
|
353
|
+
override fun postDelayed(
|
|
354
|
+
action: Runnable?,
|
|
355
|
+
delayMillis: Long,
|
|
356
|
+
): Boolean {
|
|
357
|
+
if (contextMenuHidden && isRomSelectionPopupShower(action)) return false
|
|
358
|
+
return super.postDelayed(action, delayMillis)
|
|
359
|
+
}
|
|
360
|
+
|
|
339
361
|
/** Clears the selection (ending its ActionMode) and reports `active: false`. */
|
|
340
362
|
fun clearSelection() {
|
|
341
363
|
clearTextSelection()
|
|
@@ -7,6 +7,7 @@ import android.text.SpannableString
|
|
|
7
7
|
import android.text.StaticLayout
|
|
8
8
|
import android.text.TextPaint
|
|
9
9
|
import android.util.Log
|
|
10
|
+
import androidx.emoji2.text.EmojiCompat
|
|
10
11
|
import com.facebook.react.bridge.ReadableMap
|
|
11
12
|
import com.facebook.react.uimanager.PixelUtil
|
|
12
13
|
import com.facebook.yoga.YogaMeasureMode
|
|
@@ -92,6 +93,38 @@ object MeasurementStore {
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
private val measurePaint = TextPaint()
|
|
96
|
+
|
|
97
|
+
@Volatile
|
|
98
|
+
private var emojiInitCallbackRegistered = false
|
|
99
|
+
|
|
100
|
+
// A view measured before EmojiCompat loaded keeps a size without its emoji glyphs.
|
|
101
|
+
private val emojiInitCallback =
|
|
102
|
+
object : EmojiCompat.InitCallback() {
|
|
103
|
+
override fun onInitialized() {
|
|
104
|
+
data.clear()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// AppCompatTextView draws emoji the system font lacks through EmojiCompat, which can be
|
|
109
|
+
// wider than the fallback glyph and wrap a line earlier; measure the same glyphs.
|
|
110
|
+
private fun withEmojiSpans(text: CharSequence): CharSequence {
|
|
111
|
+
if (text.isEmpty() || !EmojiCompat.isConfigured()) return text
|
|
112
|
+
val emojiCompat = EmojiCompat.get()
|
|
113
|
+
if (emojiCompat.loadState != EmojiCompat.LOAD_STATE_SUCCEEDED) {
|
|
114
|
+
if (!emojiInitCallbackRegistered) {
|
|
115
|
+
emojiInitCallbackRegistered = true
|
|
116
|
+
emojiCompat.registerInitCallback(emojiInitCallback)
|
|
117
|
+
}
|
|
118
|
+
return text
|
|
119
|
+
}
|
|
120
|
+
return try {
|
|
121
|
+
emojiCompat.process(SpannableString(text)) ?: text
|
|
122
|
+
} catch (e: IllegalStateException) {
|
|
123
|
+
Log.w(TAG, "EmojiCompat processing failed, measuring raw text", e)
|
|
124
|
+
text
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
95
128
|
private val measureRenderer = Renderer()
|
|
96
129
|
|
|
97
130
|
@Volatile
|
|
@@ -337,9 +370,12 @@ object MeasurementStore {
|
|
|
337
370
|
tryRenderMarkdown(markdown, styleMap, context, md4cFlags, allowFontScaling, maxFontSizeMultiplier, imageRequestHeaders)
|
|
338
371
|
// Placeholders must be built with the same inlineMath.paddingVertical the real spans use,
|
|
339
372
|
// or the measured line box won't match the drawn one.
|
|
373
|
+
val inlineMathMap = styleMap.getMapOrNull("inlineMath")
|
|
374
|
+
val inlineMathOverflowDip = inlineMathMap.getFloatOrDefault("glyphOverflow", 0f)
|
|
340
375
|
spannable?.replaceMathSpansWithPlaceholders(
|
|
341
376
|
context,
|
|
342
|
-
PixelUtil.toPixelFromDIP(
|
|
377
|
+
PixelUtil.toPixelFromDIP(inlineMathMap.getFloatOrDefault("paddingVertical", 0f)) +
|
|
378
|
+
if (inlineMathOverflowDip > 0f) ceil(PixelUtil.toPixelFromDIP(inlineMathOverflowDip)) else 0f,
|
|
343
379
|
)
|
|
344
380
|
val textToMeasure = spannable ?: markdown
|
|
345
381
|
val (size, _) = measureWithLayout(width, textToMeasure, measurePaint, id)
|
|
@@ -456,7 +492,8 @@ object MeasurementStore {
|
|
|
456
492
|
for (i in mathSegmentIndices.indices) {
|
|
457
493
|
val metrics = mathResults[i]
|
|
458
494
|
mathHeightByIndex[mathSegmentIndices[i]] =
|
|
459
|
-
ceil(metrics.ascent + metrics.descent).toInt() + (style.mathStyle.padding * 2)
|
|
495
|
+
ceil(metrics.ascent + metrics.descent).toInt() + (style.mathStyle.padding * 2) +
|
|
496
|
+
(style.mathStyle.glyphOverflow * 2)
|
|
460
497
|
}
|
|
461
498
|
}
|
|
462
499
|
|
|
@@ -471,7 +508,7 @@ object MeasurementStore {
|
|
|
471
508
|
|
|
472
509
|
when (segment) {
|
|
473
510
|
is RenderedSegment.Text -> {
|
|
474
|
-
segment.styledText.replaceMathSpansWithPlaceholders(context, style.inlineMathStyle.
|
|
511
|
+
segment.styledText.replaceMathSpansWithPlaceholders(context, style.inlineMathStyle.boxPaddingVertical)
|
|
475
512
|
|
|
476
513
|
val layout = createStaticLayout(segment.styledText, fontSize, widthPx, id)
|
|
477
514
|
totalHeightPx += layout.height
|
|
@@ -546,9 +583,10 @@ object MeasurementStore {
|
|
|
546
583
|
viewId: Int?,
|
|
547
584
|
): StaticLayout {
|
|
548
585
|
measurePaint.textSize = fontSize
|
|
549
|
-
|
|
586
|
+
val content = withEmojiSpans(text)
|
|
587
|
+
prepareImageSpansForMeasurement(content, widthPx)
|
|
550
588
|
return StaticLayout.Builder
|
|
551
|
-
.obtain(
|
|
589
|
+
.obtain(content, 0, content.length, measurePaint, widthPx)
|
|
552
590
|
.setIncludePad(false)
|
|
553
591
|
.setLineSpacing(0f, 1f)
|
|
554
592
|
.apply {
|
|
@@ -579,7 +617,7 @@ object MeasurementStore {
|
|
|
579
617
|
style.imageRequestHeaders = imageRequestHeaders
|
|
580
618
|
measureRenderer.configure(style, context)
|
|
581
619
|
measureRenderer.renderDocument(ast, null).apply {
|
|
582
|
-
replaceMathSpansWithPlaceholders(context, style.inlineMathStyle.
|
|
620
|
+
replaceMathSpansWithPlaceholders(context, style.inlineMathStyle.boxPaddingVertical)
|
|
583
621
|
}
|
|
584
622
|
} catch (e: Exception) {
|
|
585
623
|
Log.w(TAG, "Failed to render markdown for measurement, falling back to raw text", e)
|
|
@@ -628,7 +666,7 @@ object MeasurementStore {
|
|
|
628
666
|
paint: TextPaint,
|
|
629
667
|
viewId: Int?,
|
|
630
668
|
): Long {
|
|
631
|
-
val content = text ?: ""
|
|
669
|
+
val content = withEmojiSpans(text ?: "")
|
|
632
670
|
val safeWidth = ceil(maxWidth).toInt().coerceAtLeast(1)
|
|
633
671
|
prepareImageSpansForMeasurement(content, safeWidth)
|
|
634
672
|
|
|
@@ -668,7 +706,7 @@ object MeasurementStore {
|
|
|
668
706
|
paint: TextPaint,
|
|
669
707
|
viewId: Int?,
|
|
670
708
|
): Pair<Long, StaticLayout> {
|
|
671
|
-
val content = text ?: ""
|
|
709
|
+
val content = withEmojiSpans(text ?: "")
|
|
672
710
|
val widthPx = ceil(maxWidth).toInt().coerceAtLeast(1)
|
|
673
711
|
prepareImageSpansForMeasurement(content, widthPx)
|
|
674
712
|
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
package com.swmansion.enriched.markdown.styles
|
|
2
2
|
|
|
3
3
|
import com.facebook.react.bridge.ReadableMap
|
|
4
|
+
import kotlin.math.ceil
|
|
4
5
|
|
|
5
6
|
data class InlineMathStyle(
|
|
6
7
|
val color: Int,
|
|
7
8
|
val paddingVertical: Float,
|
|
8
9
|
val fontWeight: String,
|
|
10
|
+
val glyphOverflow: Float = 0f,
|
|
9
11
|
) {
|
|
12
|
+
/** Line-box padding the span reports: author padding plus the ink headroom. */
|
|
13
|
+
val boxPaddingVertical: Float get() = paddingVertical + glyphOverflow
|
|
14
|
+
|
|
10
15
|
companion object {
|
|
11
16
|
fun fromReadableMap(
|
|
12
17
|
map: ReadableMap,
|
|
@@ -15,7 +20,17 @@ data class InlineMathStyle(
|
|
|
15
20
|
val color = parser.parseColor(map, "color")
|
|
16
21
|
val paddingVertical = parser.toPixelFromDIP(map.getDouble("paddingVertical").toFloat())
|
|
17
22
|
val fontWeight = parser.parseString(map, "fontWeight", "normal")
|
|
18
|
-
|
|
23
|
+
val glyphOverflow = glyphOverflowPx(parser.parseOptionalDouble(map, "glyphOverflow").toFloat(), parser)
|
|
24
|
+
return InlineMathStyle(color, paddingVertical, fontWeight, glyphOverflow)
|
|
19
25
|
}
|
|
20
26
|
}
|
|
21
27
|
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `glyphOverflow` in whole pixels. Rounded up so a measured box and the bitmap or
|
|
31
|
+
* canvas it holds always grow by the same integer amount on each side.
|
|
32
|
+
*/
|
|
33
|
+
internal fun glyphOverflowPx(
|
|
34
|
+
dip: Float,
|
|
35
|
+
parser: StyleParser,
|
|
36
|
+
): Float = if (dip <= 0f) 0f else ceil(parser.toPixelFromDIP(dip))
|
|
@@ -11,6 +11,7 @@ data class MathStyle(
|
|
|
11
11
|
val marginBottom: Float,
|
|
12
12
|
val textAlign: String,
|
|
13
13
|
val fontWeight: String,
|
|
14
|
+
val glyphOverflow: Float = 0f,
|
|
14
15
|
) {
|
|
15
16
|
companion object {
|
|
16
17
|
fun fromReadableMap(
|
|
@@ -25,6 +26,7 @@ data class MathStyle(
|
|
|
25
26
|
val marginBottom = parser.toPixelFromDIP(map.getDouble("marginBottom").toFloat())
|
|
26
27
|
val textAlign = parser.parseString(map, "textAlign", "center")
|
|
27
28
|
val fontWeight = parser.parseString(map, "fontWeight", "normal")
|
|
29
|
+
val glyphOverflow = glyphOverflowPx(parser.parseOptionalDouble(map, "glyphOverflow").toFloat(), parser)
|
|
28
30
|
return MathStyle(
|
|
29
31
|
fontSize,
|
|
30
32
|
color,
|
|
@@ -34,6 +36,7 @@ data class MathStyle(
|
|
|
34
36
|
marginBottom,
|
|
35
37
|
textAlign,
|
|
36
38
|
fontWeight,
|
|
39
|
+
glyphOverflow,
|
|
37
40
|
)
|
|
38
41
|
}
|
|
39
42
|
}
|
package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/HeadlessActionMode.kt
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.utils.text.view
|
|
2
|
+
|
|
3
|
+
import android.view.ActionMode
|
|
4
|
+
import android.view.Menu
|
|
5
|
+
import android.view.MenuInflater
|
|
6
|
+
import android.view.View
|
|
7
|
+
import android.widget.PopupMenu
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Stands in for the floating text-selection toolbar while `contextMenuHidden`
|
|
11
|
+
* is on. An empty menu only hides the AOSP FloatingToolbar; ROMs that draw
|
|
12
|
+
* their own selection popup (MIUI) still show it. Answering the Editor's
|
|
13
|
+
* startActionMode with this mode means no toolbar window is ever created.
|
|
14
|
+
*
|
|
15
|
+
* The callback is the Editor's TextActionModeCallback wrapping ours, so it
|
|
16
|
+
* still gets the full create/prepare/destroy lifecycle: Editor keeps the
|
|
17
|
+
* selection handles and its transient-state bookkeeping, and finish() clears
|
|
18
|
+
* the selection exactly as dismissing the real toolbar would.
|
|
19
|
+
*/
|
|
20
|
+
internal class HeadlessActionMode private constructor(
|
|
21
|
+
private val view: View,
|
|
22
|
+
private val callback: ActionMode.Callback,
|
|
23
|
+
) : ActionMode() {
|
|
24
|
+
// A real framework menu: Editor populates it with MenuItem calls
|
|
25
|
+
// (setAlphabeticShortcut, setShowAsAction, setIntent) a stub can't take.
|
|
26
|
+
private val menu: Menu = PopupMenu(view.context, view).menu
|
|
27
|
+
private var finished = false
|
|
28
|
+
|
|
29
|
+
override fun setTitle(title: CharSequence?) = Unit
|
|
30
|
+
|
|
31
|
+
override fun setTitle(resId: Int) = Unit
|
|
32
|
+
|
|
33
|
+
override fun setSubtitle(subtitle: CharSequence?) = Unit
|
|
34
|
+
|
|
35
|
+
override fun setSubtitle(resId: Int) = Unit
|
|
36
|
+
|
|
37
|
+
override fun setCustomView(view: View?) = Unit
|
|
38
|
+
|
|
39
|
+
override fun invalidate() {
|
|
40
|
+
if (!finished) callback.onPrepareActionMode(this, menu)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
override fun invalidateContentRect() = Unit
|
|
44
|
+
|
|
45
|
+
override fun hide(duration: Long) = Unit
|
|
46
|
+
|
|
47
|
+
override fun finish() {
|
|
48
|
+
if (finished) return
|
|
49
|
+
finished = true
|
|
50
|
+
callback.onDestroyActionMode(this)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override fun getMenu(): Menu = menu
|
|
54
|
+
|
|
55
|
+
override fun getTitle(): CharSequence? = null
|
|
56
|
+
|
|
57
|
+
override fun getSubtitle(): CharSequence? = null
|
|
58
|
+
|
|
59
|
+
override fun getCustomView(): View? = null
|
|
60
|
+
|
|
61
|
+
override fun getMenuInflater(): MenuInflater = MenuInflater(view.context)
|
|
62
|
+
|
|
63
|
+
companion object {
|
|
64
|
+
/** Mirrors the framework: null when the callback declines to start. */
|
|
65
|
+
fun start(
|
|
66
|
+
view: View,
|
|
67
|
+
callback: ActionMode.Callback,
|
|
68
|
+
): ActionMode? {
|
|
69
|
+
val mode = HeadlessActionMode(view, callback)
|
|
70
|
+
mode.type = TYPE_FLOATING
|
|
71
|
+
if (!callback.onCreateActionMode(mode, mode.menu)) return null
|
|
72
|
+
callback.onPrepareActionMode(mode, mode.menu)
|
|
73
|
+
return mode
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* MIUI 12 (Android 10) replaces android.widget.Editor: text selection never reaches
|
|
80
|
+
* startActionMode or the custom ActionMode callback, so HeadlessActionMode is never used.
|
|
81
|
+
* Its own popup is shown by Editor$HandleView.showActionPopupWindow, which posts a runnable
|
|
82
|
+
* on the TextView (Editor$HandleView$2). Dropping that runnable is the only hook left while
|
|
83
|
+
* `contextMenuHidden` is on.
|
|
84
|
+
*/
|
|
85
|
+
private const val ROM_SELECTION_POPUP_SHOWER_PREFIX = "android.widget.Editor\$HandleView"
|
|
86
|
+
|
|
87
|
+
internal fun isRomSelectionPopupShower(action: Runnable?): Boolean =
|
|
88
|
+
action != null && action.javaClass.name.startsWith(ROM_SELECTION_POPUP_SHOWER_PREFIX)
|
|
@@ -506,7 +506,7 @@ class TableContainerView(
|
|
|
506
506
|
.apply { configure(config, context) }
|
|
507
507
|
.renderDocument(MarkdownASTNode(NodeType.Document, children = listOf(paragraph)), null, null)
|
|
508
508
|
}
|
|
509
|
-
styledText.replaceMathSpansWithPlaceholders(context, config.inlineMathStyle.
|
|
509
|
+
styledText.replaceMathSpansWithPlaceholders(context, config.inlineMathStyle.boxPaddingVertical)
|
|
510
510
|
if (isHeader && styledText.isNotEmpty()) {
|
|
511
511
|
styledText.setSpan(HeaderTypefaceSpan(headerTypeface), 0, styledText.length, 33)
|
|
512
512
|
}
|
package/android/src/math/java/com/swmansion/enriched/markdown/renderer/MathInlineRenderer.kt
CHANGED
|
@@ -37,6 +37,7 @@ class MathInlineRenderer(
|
|
|
37
37
|
textColor = config.style.inlineMathStyle.color,
|
|
38
38
|
paddingVertical = config.style.inlineMathStyle.paddingVertical,
|
|
39
39
|
fontWeight = config.style.inlineMathStyle.fontWeight,
|
|
40
|
+
glyphOverflow = config.style.inlineMathStyle.glyphOverflow,
|
|
40
41
|
)
|
|
41
42
|
|
|
42
43
|
builder.setSpan(span, start, end, SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE)
|
|
@@ -16,6 +16,7 @@ class MathInlineSpan(
|
|
|
16
16
|
private val textColor: Int,
|
|
17
17
|
private val paddingVertical: Float,
|
|
18
18
|
private val fontWeight: String = "normal",
|
|
19
|
+
private val glyphOverflow: Float = 0f,
|
|
19
20
|
) : ReplacementSpan() {
|
|
20
21
|
private var cachedBitmap: Bitmap? = null
|
|
21
22
|
private var cachedWidth = 0
|
|
@@ -31,18 +32,24 @@ class MathInlineSpan(
|
|
|
31
32
|
val displayList = RaTeXEngine.parseBlocking(latex, displayMode = false, color = textColor)
|
|
32
33
|
val renderer = RaTeXRenderer(displayList, fontSize, ratexTypefaceLoader(fontWeight))
|
|
33
34
|
|
|
34
|
-
cachedWidth = renderer.widthPx.toInt().coerceAtLeast(1)
|
|
35
|
+
cachedWidth = ceil(renderer.widthPx).toInt().coerceAtLeast(1)
|
|
35
36
|
mathAscent = renderer.heightPx
|
|
36
37
|
mathDescent = renderer.depthPx
|
|
37
38
|
|
|
39
|
+
// RaTeX reports TeX metric boxes, which digit and capital outlines (and the
|
|
40
|
+
// fake-bold stroke) overshoot; the bitmap keeps glyphOverflow px of headroom
|
|
41
|
+
// on both sides so that ink is not cut off.
|
|
42
|
+
val overflowPx = glyphOverflow.toInt()
|
|
38
43
|
val bitmap =
|
|
39
44
|
Bitmap.createBitmap(
|
|
40
45
|
cachedWidth,
|
|
41
|
-
ceil(renderer.totalHeightPx).toInt().coerceAtLeast(1),
|
|
46
|
+
ceil(renderer.totalHeightPx).toInt().coerceAtLeast(1) + overflowPx * 2,
|
|
42
47
|
Bitmap.Config.ARGB_8888,
|
|
43
48
|
)
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
val canvas = Canvas(bitmap)
|
|
51
|
+
canvas.translate(0f, overflowPx.toFloat())
|
|
52
|
+
renderer.draw(canvas)
|
|
46
53
|
cachedBitmap = bitmap
|
|
47
54
|
} catch (_: Exception) {
|
|
48
55
|
renderFailed = true
|
|
@@ -66,9 +73,9 @@ class MathInlineSpan(
|
|
|
66
73
|
// Must stay identical to MathInlinePlaceholderSpan.getSize: that span measures
|
|
67
74
|
// this one off the main thread, and a box that disagrees by even a pixel either
|
|
68
75
|
// clips the formula or eats the padding this field is here to add.
|
|
69
|
-
ascent = -ceil(mathAscent + paddingVertical).toInt()
|
|
76
|
+
ascent = -ceil(mathAscent + (paddingVertical + glyphOverflow)).toInt()
|
|
70
77
|
top = ascent
|
|
71
|
-
descent = ceil(mathDescent + paddingVertical).toInt()
|
|
78
|
+
descent = ceil(mathDescent + (paddingVertical + glyphOverflow)).toInt()
|
|
72
79
|
bottom = descent
|
|
73
80
|
}
|
|
74
81
|
|
|
@@ -88,7 +95,7 @@ class MathInlineSpan(
|
|
|
88
95
|
) {
|
|
89
96
|
prepareResources()
|
|
90
97
|
cachedBitmap?.let {
|
|
91
|
-
val bitmapY = y - ceil(mathAscent)
|
|
98
|
+
val bitmapY = y - ceil(mathAscent) - glyphOverflow
|
|
92
99
|
canvas.drawBitmap(it, x, bitmapY, paint)
|
|
93
100
|
}
|
|
94
101
|
}
|
|
@@ -5,6 +5,7 @@ import android.util.Log
|
|
|
5
5
|
import io.ratex.RaTeXEngine
|
|
6
6
|
import io.ratex.RaTeXFontLoader
|
|
7
7
|
import io.ratex.RaTeXRenderer
|
|
8
|
+
import kotlin.math.ceil
|
|
8
9
|
|
|
9
10
|
object MathMeasureHelper {
|
|
10
11
|
@JvmStatic
|
|
@@ -37,7 +38,7 @@ object MathMeasureHelper {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
return MathMetrics(
|
|
40
|
-
renderer.widthPx.toInt(),
|
|
41
|
+
ceil(renderer.widthPx).toInt().coerceAtLeast(1),
|
|
41
42
|
renderer.heightPx,
|
|
42
43
|
renderer.depthPx,
|
|
43
44
|
)
|
|
@@ -58,7 +58,7 @@ class MathContainerView(
|
|
|
58
58
|
else -> Gravity.CENTER_HORIZONTAL
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
private val mathView = RaTeXCanvasView(context)
|
|
61
|
+
private val mathView = RaTeXCanvasView(context, mathStyle.glyphOverflow)
|
|
62
62
|
|
|
63
63
|
init {
|
|
64
64
|
setBackgroundColor(mathStyle.backgroundColor)
|
|
@@ -139,8 +139,11 @@ class MathContainerView(
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// glyphOverflow px of headroom above and below the RaTeX box: digit and capital
|
|
143
|
+
// outlines (and the fake-bold stroke) overshoot it, and this view clips to its size.
|
|
142
144
|
private class RaTeXCanvasView(
|
|
143
145
|
context: Context,
|
|
146
|
+
private val glyphOverflow: Float,
|
|
144
147
|
) : View(context) {
|
|
145
148
|
var renderer: RaTeXRenderer? = null
|
|
146
149
|
|
|
@@ -153,12 +156,16 @@ class MathContainerView(
|
|
|
153
156
|
?: return setMeasuredDimension(0, 0)
|
|
154
157
|
setMeasuredDimension(
|
|
155
158
|
ceil(currentRenderer.widthPx).toInt().coerceAtLeast(1),
|
|
156
|
-
ceil(currentRenderer.totalHeightPx).toInt().coerceAtLeast(1),
|
|
159
|
+
ceil(currentRenderer.totalHeightPx).toInt().coerceAtLeast(1) + (glyphOverflow * 2).toInt(),
|
|
157
160
|
)
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
override fun onDraw(canvas: Canvas) {
|
|
161
|
-
renderer
|
|
164
|
+
val currentRenderer = renderer ?: return
|
|
165
|
+
canvas.save()
|
|
166
|
+
canvas.translate(0f, glyphOverflow)
|
|
167
|
+
currentRenderer.draw(canvas)
|
|
168
|
+
canvas.restore()
|
|
162
169
|
}
|
|
163
170
|
}
|
|
164
171
|
|
|
@@ -175,7 +182,7 @@ class MathContainerView(
|
|
|
175
182
|
mode = MathRenderMode.Display,
|
|
176
183
|
)
|
|
177
184
|
val metrics = MathMeasureHelper.measure(context, listOf(request)).first()
|
|
178
|
-
return ceil(metrics.ascent + metrics.descent).toInt() + (mathStyle.padding * 2)
|
|
185
|
+
return ceil(metrics.ascent + metrics.descent).toInt() + (mathStyle.padding * 2) + (mathStyle.glyphOverflow * 2)
|
|
179
186
|
}
|
|
180
187
|
|
|
181
188
|
private const val TAG = "MathContainerView"
|
|
@@ -14,6 +14,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
14
14
|
/// Typeset the glyphs at bold weight (synthetic — see ENRMRaTeXBridge).
|
|
15
15
|
@property (nonatomic, assign) BOOL bold;
|
|
16
16
|
|
|
17
|
+
/// Ink headroom above and below the formula box (`inlineMath.glyphOverflow`, points).
|
|
18
|
+
@property (nonatomic, assign) CGFloat glyphOverflow;
|
|
19
|
+
|
|
17
20
|
/// YES if block math (`$$...$$`), NO for inline (`$...$`).
|
|
18
21
|
@property (nonatomic, assign) BOOL displayMode;
|
|
19
22
|
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
displayMode:NO
|
|
43
43
|
fontSize:self.fontSize
|
|
44
44
|
color:color
|
|
45
|
-
bold:self.bold
|
|
45
|
+
bold:self.bold
|
|
46
|
+
glyphOverflow:self.glyphOverflow];
|
|
46
47
|
if (!result) {
|
|
47
48
|
_fallbackSource = ENRMMathFallbackString(self.latex, @"$", self.fontSize, color);
|
|
48
49
|
UIFont *font = [_fallbackSource attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL];
|