react-native-enriched-markdown-scaffold 1.0.2-scaffold.6 → 1.0.2-scaffold.8
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/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +2 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +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 +12 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +12 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +8 -4
- package/android/src/main/java/com/swmansion/enriched/markdown/events/SelectionChangeEvent.kt +1 -0
- 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 +76 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionChangeReporter.kt +2 -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/docs/API_REFERENCE.md +4 -2
- package/ios/EnrichedMarkdown.mm +23 -0
- package/ios/EnrichedMarkdownText.mm +21 -0
- 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/EventEmitters.cpp +2 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +2 -0
- 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/ENRMLinkChip.m +2 -1
- package/ios/utils/ENRMSelectionChange.h +24 -0
- package/ios/utils/ENRMSelectionChange.m +78 -0
- package/ios/utils/StylePropsUtils.h +10 -0
- package/ios/views/ENRMMathContainerView.m +4 -2
- package/lib/module/EnrichedMarkdownNativeComponent.ts +3 -0
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +3 -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 +3 -0
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.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/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 +8 -2
- package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
- package/lib/typescript/src/types/events.d.ts +8 -0
- package/lib/typescript/src/types/events.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/EnrichedMarkdownNativeComponent.ts +3 -0
- package/src/EnrichedMarkdownTextNativeComponent.ts +3 -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 +8 -2
- package/src/types/events.ts +8 -0
package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp
CHANGED
|
@@ -87,6 +87,7 @@ payload.setProperty(runtime, "whole", event.whole);
|
|
|
87
87
|
endRect.setProperty(runtime, "height", event.endRect.height);
|
|
88
88
|
payload.setProperty(runtime, "endRect", endRect);
|
|
89
89
|
}
|
|
90
|
+
payload.setProperty(runtime, "adjusting", event.adjusting);
|
|
90
91
|
return payload;
|
|
91
92
|
});
|
|
92
93
|
}
|
|
@@ -487,6 +488,7 @@ payload.setProperty(runtime, "whole", event.whole);
|
|
|
487
488
|
endRect.setProperty(runtime, "height", event.endRect.height);
|
|
488
489
|
payload.setProperty(runtime, "endRect", endRect);
|
|
489
490
|
}
|
|
491
|
+
payload.setProperty(runtime, "adjusting", event.adjusting);
|
|
490
492
|
return payload;
|
|
491
493
|
});
|
|
492
494
|
}
|
package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h
CHANGED
|
@@ -69,6 +69,7 @@ class EnrichedMarkdownEventEmitter : public ViewEventEmitter {
|
|
|
69
69
|
OnSelectionChangeRect rect;
|
|
70
70
|
OnSelectionChangeStartRect startRect;
|
|
71
71
|
OnSelectionChangeEndRect endRect;
|
|
72
|
+
bool adjusting;
|
|
72
73
|
};
|
|
73
74
|
|
|
74
75
|
struct OnTaskListItemPress {
|
|
@@ -395,6 +396,7 @@ class EnrichedMarkdownTextEventEmitter : public ViewEventEmitter {
|
|
|
395
396
|
OnSelectionChangeRect rect;
|
|
396
397
|
OnSelectionChangeStartRect startRect;
|
|
397
398
|
OnSelectionChangeEndRect endRect;
|
|
399
|
+
bool adjusting;
|
|
398
400
|
};
|
|
399
401
|
|
|
400
402
|
struct OnTaskListItemPress {
|
|
@@ -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
|
|
@@ -128,6 +129,17 @@ class EnrichedMarkdownInternalText
|
|
|
128
129
|
onSelectionChangedCallback?.invoke(this, selEnd > selStart)
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
// See HeadlessActionMode: with the menu hidden, no toolbar window is created.
|
|
133
|
+
override fun startActionMode(
|
|
134
|
+
callback: ActionMode.Callback?,
|
|
135
|
+
type: Int,
|
|
136
|
+
): ActionMode? {
|
|
137
|
+
if (contextMenuHidden && type == ActionMode.TYPE_FLOATING && callback != null) {
|
|
138
|
+
return HeadlessActionMode.start(this, callback)
|
|
139
|
+
}
|
|
140
|
+
return super.startActionMode(callback, type)
|
|
141
|
+
}
|
|
142
|
+
|
|
131
143
|
/** Clears the selection and ends its ActionMode. */
|
|
132
144
|
fun clearSelection() {
|
|
133
145
|
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
|
|
@@ -336,6 +337,17 @@ class EnrichedMarkdownText
|
|
|
336
337
|
selectionReporter?.enabled = enabled
|
|
337
338
|
}
|
|
338
339
|
|
|
340
|
+
// See HeadlessActionMode: with the menu hidden, no toolbar window is created.
|
|
341
|
+
override fun startActionMode(
|
|
342
|
+
callback: ActionMode.Callback?,
|
|
343
|
+
type: Int,
|
|
344
|
+
): ActionMode? {
|
|
345
|
+
if (contextMenuHidden && type == ActionMode.TYPE_FLOATING && callback != null) {
|
|
346
|
+
return HeadlessActionMode.start(this, callback)
|
|
347
|
+
}
|
|
348
|
+
return super.startActionMode(callback, type)
|
|
349
|
+
}
|
|
350
|
+
|
|
339
351
|
/** Clears the selection (ending its ActionMode) and reports `active: false`. */
|
|
340
352
|
fun clearSelection() {
|
|
341
353
|
clearTextSelection()
|
|
@@ -337,9 +337,12 @@ object MeasurementStore {
|
|
|
337
337
|
tryRenderMarkdown(markdown, styleMap, context, md4cFlags, allowFontScaling, maxFontSizeMultiplier, imageRequestHeaders)
|
|
338
338
|
// Placeholders must be built with the same inlineMath.paddingVertical the real spans use,
|
|
339
339
|
// or the measured line box won't match the drawn one.
|
|
340
|
+
val inlineMathMap = styleMap.getMapOrNull("inlineMath")
|
|
341
|
+
val inlineMathOverflowDip = inlineMathMap.getFloatOrDefault("glyphOverflow", 0f)
|
|
340
342
|
spannable?.replaceMathSpansWithPlaceholders(
|
|
341
343
|
context,
|
|
342
|
-
PixelUtil.toPixelFromDIP(
|
|
344
|
+
PixelUtil.toPixelFromDIP(inlineMathMap.getFloatOrDefault("paddingVertical", 0f)) +
|
|
345
|
+
if (inlineMathOverflowDip > 0f) ceil(PixelUtil.toPixelFromDIP(inlineMathOverflowDip)) else 0f,
|
|
343
346
|
)
|
|
344
347
|
val textToMeasure = spannable ?: markdown
|
|
345
348
|
val (size, _) = measureWithLayout(width, textToMeasure, measurePaint, id)
|
|
@@ -456,7 +459,8 @@ object MeasurementStore {
|
|
|
456
459
|
for (i in mathSegmentIndices.indices) {
|
|
457
460
|
val metrics = mathResults[i]
|
|
458
461
|
mathHeightByIndex[mathSegmentIndices[i]] =
|
|
459
|
-
ceil(metrics.ascent + metrics.descent).toInt() + (style.mathStyle.padding * 2)
|
|
462
|
+
ceil(metrics.ascent + metrics.descent).toInt() + (style.mathStyle.padding * 2) +
|
|
463
|
+
(style.mathStyle.glyphOverflow * 2)
|
|
460
464
|
}
|
|
461
465
|
}
|
|
462
466
|
|
|
@@ -471,7 +475,7 @@ object MeasurementStore {
|
|
|
471
475
|
|
|
472
476
|
when (segment) {
|
|
473
477
|
is RenderedSegment.Text -> {
|
|
474
|
-
segment.styledText.replaceMathSpansWithPlaceholders(context, style.inlineMathStyle.
|
|
478
|
+
segment.styledText.replaceMathSpansWithPlaceholders(context, style.inlineMathStyle.boxPaddingVertical)
|
|
475
479
|
|
|
476
480
|
val layout = createStaticLayout(segment.styledText, fontSize, widthPx, id)
|
|
477
481
|
totalHeightPx += layout.height
|
|
@@ -579,7 +583,7 @@ object MeasurementStore {
|
|
|
579
583
|
style.imageRequestHeaders = imageRequestHeaders
|
|
580
584
|
measureRenderer.configure(style, context)
|
|
581
585
|
measureRenderer.renderDocument(ast, null).apply {
|
|
582
|
-
replaceMathSpansWithPlaceholders(context, style.inlineMathStyle.
|
|
586
|
+
replaceMathSpansWithPlaceholders(context, style.inlineMathStyle.boxPaddingVertical)
|
|
583
587
|
}
|
|
584
588
|
} catch (e: Exception) {
|
|
585
589
|
Log.w(TAG, "Failed to render markdown for measurement, falling back to raw text", e)
|
package/android/src/main/java/com/swmansion/enriched/markdown/events/SelectionChangeEvent.kt
CHANGED
|
@@ -26,6 +26,7 @@ class SelectionChangeEvent(
|
|
|
26
26
|
putMap("rect", snapshot.rect.toWritableMap())
|
|
27
27
|
putMap("startRect", snapshot.startRect.toWritableMap())
|
|
28
28
|
putMap("endRect", snapshot.endRect.toWritableMap())
|
|
29
|
+
putBoolean("adjusting", snapshot.adjusting)
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
private fun SelectionRect.toWritableMap(): WritableMap =
|
|
@@ -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,76 @@
|
|
|
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
|
+
}
|
|
@@ -46,6 +46,8 @@ data class SelectionSnapshot(
|
|
|
46
46
|
val rect: SelectionRect,
|
|
47
47
|
val startRect: SelectionRect,
|
|
48
48
|
val endRect: SelectionRect,
|
|
49
|
+
// Not tracked on Android: the selection is reported continuously while a handle is dragged.
|
|
50
|
+
val adjusting: Boolean = false,
|
|
49
51
|
) {
|
|
50
52
|
companion object {
|
|
51
53
|
val INACTIVE =
|
|
@@ -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"
|
package/docs/API_REFERENCE.md
CHANGED
|
@@ -248,13 +248,15 @@ interface SelectionChangeEvent {
|
|
|
248
248
|
/** Selected part of the first / last line — anchor a menu above the start or below the end. */
|
|
249
249
|
startRect: SelectionRect;
|
|
250
250
|
endRect: SelectionRect;
|
|
251
|
+
/** `true` while a selection handle is dragged (iOS; always `false` on Android). */
|
|
252
|
+
adjusting: boolean;
|
|
251
253
|
}
|
|
252
254
|
```
|
|
253
255
|
|
|
254
256
|
- Fires at most once per frame while the selection changes (payloads are deduplicated), and with `active: false` right away once it is gone — including when new `markdown` replaces the text.
|
|
255
257
|
- Rects are in dp, relative to the component's native view: add the origin from `ref.current.measureInWindow` to get window coordinates. Re-measure after scrolling instead of waiting for a new event.
|
|
256
258
|
- With `contextMenuHidden`, a long press on a table, math or code block (`flavor="github"`) fires the event with `source` set to the block kind, `whole: true` and every rect set to the block's frame. These blocks have no text selection, so without this they would offer no way to copy. Without `contextMenuHidden` the block menu shows as usual and no event fires.
|
|
257
|
-
-
|
|
259
|
+
- On iOS the selection isn't reported while a handle is dragged: the event fires with `adjusting: true` (and the selection as it was) when the drag begins, and with `adjusting: false` and the new selection when the finger lifts. Hide your menu while `adjusting` is `true`. Android has no such flag (`adjusting` is always `false`) but reports every step of the drag, so debounce before showing the menu there too.
|
|
258
260
|
- Within one component only one part holds a selection at a time: selecting in another text segment clears the previous one. To keep a single selection across several components, call `clearSelection()` on the previous one.
|
|
259
261
|
- Ignored on macOS and web.
|
|
260
262
|
|
|
@@ -268,7 +270,7 @@ const ref = useRef<EnrichedMarkdownTextInstance>(null);
|
|
|
268
270
|
markdown={content}
|
|
269
271
|
contextMenuHidden
|
|
270
272
|
onSelectionChange={(e) => {
|
|
271
|
-
if (!e.active) return hideMenu();
|
|
273
|
+
if (!e.active || e.adjusting) return hideMenu();
|
|
272
274
|
ref.current?.measureInWindow((x, y) =>
|
|
273
275
|
showMenuDebounced({ x: x + e.startRect.x, y: y + e.startRect.y }, e.markdown)
|
|
274
276
|
);
|
package/ios/EnrichedMarkdown.mm
CHANGED
|
@@ -131,6 +131,9 @@ static char kENRMSegmentFadeAnimatorKey;
|
|
|
131
131
|
NSArray<NSDictionary *> *_lastLinkLayout;
|
|
132
132
|
|
|
133
133
|
ENRMSelectionChangeReporter *_selectionReporter;
|
|
134
|
+
#if !TARGET_OS_OSX
|
|
135
|
+
ENRMSelectionGestureTracker *_selectionGestures;
|
|
136
|
+
#endif
|
|
134
137
|
// At most one segment holds a selection; a new one clears the previous.
|
|
135
138
|
__weak ENRMPlatformTextView *_selectionOwner;
|
|
136
139
|
// A block long-pressed while contextMenuHidden is on, reported as the selection.
|
|
@@ -225,6 +228,20 @@ static char kENRMSegmentFadeAnimatorKey;
|
|
|
225
228
|
emitter->onSelectionChange(
|
|
226
229
|
ENRMSelectionChangeEventFromPayload<EnrichedMarkdownEventEmitter::OnSelectionChange>(payload));
|
|
227
230
|
}];
|
|
231
|
+
#if !TARGET_OS_OSX
|
|
232
|
+
// A handle drag starting or ending is reported right away, even when the
|
|
233
|
+
// selection itself is unchanged.
|
|
234
|
+
_selectionGestures = [[ENRMSelectionGestureTracker alloc] initWithChange:^{
|
|
235
|
+
EnrichedMarkdown *strongSelf = weakSelf;
|
|
236
|
+
if (!strongSelf)
|
|
237
|
+
return;
|
|
238
|
+
UITextView *owner = strongSelf->_selectionOwner;
|
|
239
|
+
if (owner)
|
|
240
|
+
[strongSelf->_selectionGestures trackTextView:owner];
|
|
241
|
+
[strongSelf->_selectionReporter forgetLastReported];
|
|
242
|
+
[strongSelf->_selectionReporter flushNow];
|
|
243
|
+
}];
|
|
244
|
+
#endif
|
|
228
245
|
}
|
|
229
246
|
return self;
|
|
230
247
|
}
|
|
@@ -262,6 +279,7 @@ static char kENRMSegmentFadeAnimatorKey;
|
|
|
262
279
|
payload[ENRMSelectionSourceKey] = @"selection";
|
|
263
280
|
payload[ENRMSelectionMarkdownKey] = markdown.length > 0 ? markdown : text;
|
|
264
281
|
payload[ENRMSelectionWholeKey] = @(whole);
|
|
282
|
+
payload[ENRMSelectionAdjustingKey] = @(_selectionGestures.adjusting);
|
|
265
283
|
return payload;
|
|
266
284
|
#else
|
|
267
285
|
return ENRMInactiveSelectionPayload();
|
|
@@ -915,6 +933,7 @@ static char kENRMSegmentFadeAnimatorKey;
|
|
|
915
933
|
|
|
916
934
|
#if !TARGET_OS_OSX
|
|
917
935
|
view.textView.delegate = self;
|
|
936
|
+
[_selectionGestures trackTextView:view.textView];
|
|
918
937
|
#else
|
|
919
938
|
__weak EnrichedMarkdown *weakSelf = self;
|
|
920
939
|
[view setContextMenuProvider:^NSMenu *_Nullable(NSMenu *baseMenu, NSTextView *textView) {
|
|
@@ -1312,6 +1331,9 @@ static char kENRMSegmentFadeAnimatorKey;
|
|
|
1312
1331
|
_emitLinkLayout = NO;
|
|
1313
1332
|
_lastLinkLayout = nil;
|
|
1314
1333
|
_selectionReporter.enabled = NO;
|
|
1334
|
+
#if !TARGET_OS_OSX
|
|
1335
|
+
[_selectionGestures reset];
|
|
1336
|
+
#endif
|
|
1315
1337
|
_selectionOwner = nil;
|
|
1316
1338
|
[self clearActiveBlock];
|
|
1317
1339
|
_md4cFlags = [EnrichedMarkdown flagsFromProps:resetProps->md4cFlags];
|
|
@@ -1538,6 +1560,7 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
|
|
|
1538
1560
|
#if !TARGET_OS_OSX
|
|
1539
1561
|
- (void)textViewDidChangeSelection:(UITextView *)textView
|
|
1540
1562
|
{
|
|
1563
|
+
[_selectionGestures trackTextView:textView];
|
|
1541
1564
|
ENRMDismissSelectionEditMenuIfMoved(textView);
|
|
1542
1565
|
if (textView.selectedRange.length > 0) {
|
|
1543
1566
|
// Take ownership first so the previous owner's own "cleared" callback is ignored.
|