react-native-enriched-markdown-scaffold 1.0.2-scaffold.1 → 1.0.2-scaffold.10
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/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +18 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +4 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +18 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +4 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +148 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +120 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +30 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +210 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +176 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +45 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +35 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +112 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +34 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +46 -8
- package/android/src/main/java/com/swmansion/enriched/markdown/events/CopyRequestEvent.kt +28 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLayoutEvent.kt +36 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/events/SelectionChangeEvent.kt +43 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +25 -3
- package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/InputConnectionWrapper.kt +7 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +37 -7
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +5 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +13 -99
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/InlineChipPainter.kt +140 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkChipSpans.kt +177 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +1 -6
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineMathStyle.kt +16 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkStyle.kt +36 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkVariantEntry.kt +7 -3
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/MathStyle.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +51 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/PasteTextUtils.kt +34 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/HTMLGenerator.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/MarkdownExtractor.kt +6 -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/utils/text/view/LinkLayoutCollector.kt +100 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +53 -14
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionChangeReporter.kt +232 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/views/BlockMenuOptions.kt +64 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +29 -3
- package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +30 -7
- 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 +25 -9
- package/docs/API_REFERENCE.md +183 -4
- package/docs/COPY_OPTIONS.md +40 -0
- package/docs/INPUT.md +3 -1
- package/docs/MENTIONS.md +8 -1
- package/docs/STYLES.md +27 -0
- package/docs/TEXT.md +2 -0
- package/docs/WEB.md +1 -0
- package/ios/EnrichedMarkdown.mm +321 -9
- package/ios/EnrichedMarkdownText.mm +203 -6
- 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 +148 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +120 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +30 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +210 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +50 -2
- package/ios/internals/MeasurementCache.h +8 -2
- package/ios/math/ENRMRaTeXBridge.swift +23 -5
- package/ios/renderer/ENRMMathInlineRenderer.m +1 -0
- package/ios/renderer/LinkRenderer.m +47 -15
- package/ios/styles/StyleConfig.h +20 -0
- package/ios/styles/StyleConfig.mm +84 -0
- package/ios/utils/CodeBackground.m +3 -65
- package/ios/utils/ENRMBlockMenu.h +66 -0
- package/ios/utils/ENRMChipDrawing.h +24 -0
- package/ios/utils/ENRMChipDrawing.m +83 -0
- package/ios/utils/ENRMLinkChip.h +70 -0
- package/ios/utils/ENRMLinkChip.m +213 -0
- package/ios/utils/ENRMLinkLayout.h +50 -0
- package/ios/utils/ENRMLinkLayout.m +45 -0
- package/ios/utils/ENRMSelectionChange.h +120 -0
- package/ios/utils/ENRMSelectionChange.m +282 -0
- package/ios/utils/EditMenuUtils+macOS.m +4 -1
- package/ios/utils/EditMenuUtils.h +35 -6
- package/ios/utils/EditMenuUtils.m +171 -15
- package/ios/utils/HTMLGenerator.m +2 -1
- package/ios/utils/MarkdownAccessibilityElementBuilder.m +3 -1
- package/ios/utils/MarkdownExtractor.m +2 -1
- package/ios/utils/RTFExportUtils.m +2 -0
- package/ios/utils/StylePropsUtils.h +47 -1
- package/ios/utils/TextViewLayoutManager.mm +3 -0
- package/ios/views/ENRMCodeBlockContainerView.h +16 -2
- package/ios/views/ENRMCodeBlockContainerView.m +22 -19
- package/ios/views/ENRMMathContainerView.h +11 -0
- package/ios/views/ENRMMathContainerView.m +17 -20
- package/ios/views/TableContainerView.h +14 -0
- package/ios/views/TableContainerView.m +34 -18
- package/lib/module/EnrichedMarkdownNativeComponent.ts +138 -1
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +138 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/jest/index.js +14 -0
- package/lib/module/jest/index.js.map +1 -1
- package/lib/module/native/EnrichedMarkdownText.js +169 -11
- package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
- package/lib/module/normalizeMarkdownStyle.js +17 -4
- package/lib/module/normalizeMarkdownStyle.js.map +1 -1
- package/lib/module/normalizeMarkdownStyle.web.js +17 -4
- package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
- package/lib/module/web/EnrichedMarkdownText.js +6 -3
- package/lib/module/web/EnrichedMarkdownText.js.map +1 -1
- package/lib/module/web/renderers/InlineRenderers.js +96 -3
- package/lib/module/web/renderers/InlineRenderers.js.map +1 -1
- package/lib/module/web/styles.js +18 -0
- package/lib/module/web/styles.js.map +1 -1
- package/lib/module/web/utils.js +13 -0
- package/lib/module/web/utils.js.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +123 -2
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +123 -2
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/index.web.d.ts +1 -1
- package/lib/typescript/src/index.web.d.ts.map +1 -1
- package/lib/typescript/src/jest/index.d.ts +1 -1
- package/lib/typescript/src/jest/index.d.ts.map +1 -1
- package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +5 -5
- package/lib/typescript/src/native/EnrichedMarkdownText.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 +50 -5
- package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +12 -0
- package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownTextProps.d.ts +144 -11
- package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownTextProps.web.d.ts +41 -1
- package/lib/typescript/src/types/MarkdownTextProps.web.d.ts.map +1 -1
- package/lib/typescript/src/types/events.d.ts +66 -0
- package/lib/typescript/src/types/events.d.ts.map +1 -1
- package/lib/typescript/src/web/EnrichedMarkdownText.d.ts +1 -1
- package/lib/typescript/src/web/EnrichedMarkdownText.d.ts.map +1 -1
- package/lib/typescript/src/web/renderers/InlineRenderers.d.ts.map +1 -1
- package/lib/typescript/src/web/styles.d.ts.map +1 -1
- package/lib/typescript/src/web/types.d.ts +4 -0
- package/lib/typescript/src/web/types.d.ts.map +1 -1
- package/lib/typescript/src/web/utils.d.ts +4 -0
- package/lib/typescript/src/web/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/EnrichedMarkdownNativeComponent.ts +138 -1
- package/src/EnrichedMarkdownTextNativeComponent.ts +138 -1
- package/src/index.tsx +6 -0
- package/src/index.web.tsx +5 -1
- package/src/jest/index.tsx +22 -1
- package/src/native/EnrichedMarkdownText.tsx +220 -9
- package/src/normalizeMarkdownStyle.ts +15 -0
- package/src/normalizeMarkdownStyle.web.ts +13 -0
- package/src/types/MarkdownStyle.ts +51 -5
- package/src/types/MarkdownStyleInternal.ts +12 -0
- package/src/types/MarkdownTextProps.ts +151 -11
- package/src/types/MarkdownTextProps.web.ts +43 -1
- package/src/types/events.ts +69 -0
- package/src/web/EnrichedMarkdownText.tsx +10 -3
- package/src/web/renderers/InlineRenderers.tsx +111 -3
- package/src/web/styles.ts +25 -0
- package/src/web/types.ts +4 -0
- package/src/web/utils.ts +8 -0
package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkLayoutCollector.kt
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.utils.text.view
|
|
2
|
+
|
|
3
|
+
import android.text.Layout
|
|
4
|
+
import android.text.Spanned
|
|
5
|
+
import android.widget.TextView
|
|
6
|
+
import com.facebook.react.uimanager.PixelUtil
|
|
7
|
+
import com.swmansion.enriched.markdown.spans.LinkChipBackgroundSpan
|
|
8
|
+
import com.swmansion.enriched.markdown.spans.LinkSpacers
|
|
9
|
+
import com.swmansion.enriched.markdown.spans.LinkSpan
|
|
10
|
+
import kotlin.math.max
|
|
11
|
+
import kotlin.math.min
|
|
12
|
+
|
|
13
|
+
/** One laid-out line of a link, in dp relative to the reporting view. Covers the drawn chip. */
|
|
14
|
+
data class LinkLayoutFragment(
|
|
15
|
+
val linkIndex: Int,
|
|
16
|
+
val url: String,
|
|
17
|
+
val text: String,
|
|
18
|
+
val x: Float,
|
|
19
|
+
val y: Float,
|
|
20
|
+
val width: Float,
|
|
21
|
+
val height: Float,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
object LinkLayoutCollector {
|
|
25
|
+
/**
|
|
26
|
+
* Collects the chip rect of every link fragment laid out in [textView], for `onLinkLayout`.
|
|
27
|
+
* Rects are offset by the text view's own padding plus [offsetXPx]/[offsetYPx] (a segment's
|
|
28
|
+
* position inside its container), then converted to dp. Links are numbered in document order
|
|
29
|
+
* from [indexBase]; returns the fragments and the number of links found.
|
|
30
|
+
*/
|
|
31
|
+
fun collect(
|
|
32
|
+
textView: TextView,
|
|
33
|
+
offsetXPx: Int = 0,
|
|
34
|
+
offsetYPx: Int = 0,
|
|
35
|
+
indexBase: Int = 0,
|
|
36
|
+
): Pair<List<LinkLayoutFragment>, Int> {
|
|
37
|
+
val layout = textView.layout ?: return emptyList<LinkLayoutFragment>() to 0
|
|
38
|
+
val text = textView.text as? Spanned ?: return emptyList<LinkLayoutFragment>() to 0
|
|
39
|
+
|
|
40
|
+
// LinkRenderer sets one LinkChipBackgroundSpan per link, spacers included.
|
|
41
|
+
val chips =
|
|
42
|
+
text
|
|
43
|
+
.getSpans(0, text.length, LinkChipBackgroundSpan::class.java)
|
|
44
|
+
.sortedBy { text.getSpanStart(it) }
|
|
45
|
+
val originX = offsetXPx + textView.totalPaddingLeft
|
|
46
|
+
val originY = offsetYPx + textView.totalPaddingTop
|
|
47
|
+
|
|
48
|
+
val fragments = mutableListOf<LinkLayoutFragment>()
|
|
49
|
+
chips.forEachIndexed { i, chipSpan ->
|
|
50
|
+
val start = text.getSpanStart(chipSpan)
|
|
51
|
+
val end = text.getSpanEnd(chipSpan)
|
|
52
|
+
if (start !in 0 until end) return@forEachIndexed
|
|
53
|
+
|
|
54
|
+
val url = text.getSpans(start, end, LinkSpan::class.java).firstOrNull()?.url ?: ""
|
|
55
|
+
val linkText = LinkSpacers.strip(text.subSequence(start, end)).toString()
|
|
56
|
+
val (ascent, descent) = LinkChipBackgroundSpan.linkFontMetrics(text, start, end, textView.paint)
|
|
57
|
+
val paddingVertical = chipSpan.chip.paddingVertical
|
|
58
|
+
|
|
59
|
+
for (line in layout.getLineForOffset(start)..layout.getLineForOffset(end - 1)) {
|
|
60
|
+
val fragmentStart = max(start, layout.getLineStart(line))
|
|
61
|
+
val fragmentEnd =
|
|
62
|
+
LinkChipBackgroundSpan.trimTrailingWhitespace(text, fragmentStart, min(end, layout.getLineEnd(line)))
|
|
63
|
+
if (fragmentEnd <= fragmentStart) continue
|
|
64
|
+
|
|
65
|
+
val left = layout.getPrimaryHorizontal(fragmentStart)
|
|
66
|
+
val right = lineEndHorizontal(layout, line, fragmentEnd)
|
|
67
|
+
val baseline = layout.getLineBaseline(line).toFloat()
|
|
68
|
+
val top = baseline + ascent - paddingVertical
|
|
69
|
+
val bottom = baseline + descent + paddingVertical
|
|
70
|
+
|
|
71
|
+
fragments +=
|
|
72
|
+
LinkLayoutFragment(
|
|
73
|
+
linkIndex = indexBase + i,
|
|
74
|
+
url = url,
|
|
75
|
+
text = linkText,
|
|
76
|
+
x = PixelUtil.toDIPFromPixel(min(left, right) + originX),
|
|
77
|
+
y = PixelUtil.toDIPFromPixel(top + originY),
|
|
78
|
+
width = PixelUtil.toDIPFromPixel(kotlin.math.abs(right - left)),
|
|
79
|
+
height = PixelUtil.toDIPFromPixel(bottom - top),
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return fragments to chips.size
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* x of [offset] on [line]. At a line's end offset `getPrimaryHorizontal` reports the start
|
|
88
|
+
* of the next line (the same offset), so a fragment cut mid-word uses the line's right edge.
|
|
89
|
+
*/
|
|
90
|
+
private fun lineEndHorizontal(
|
|
91
|
+
layout: Layout,
|
|
92
|
+
line: Int,
|
|
93
|
+
offset: Int,
|
|
94
|
+
): Float =
|
|
95
|
+
if (offset >= layout.getLineEnd(line) && line < layout.lineCount - 1) {
|
|
96
|
+
layout.getLineRight(line)
|
|
97
|
+
} else {
|
|
98
|
+
layout.getPrimaryHorizontal(offset)
|
|
99
|
+
}
|
|
100
|
+
}
|
package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt
CHANGED
|
@@ -12,6 +12,7 @@ import android.widget.TextView
|
|
|
12
12
|
import com.swmansion.enriched.markdown.EnrichedMarkdown
|
|
13
13
|
import com.swmansion.enriched.markdown.EnrichedMarkdownText
|
|
14
14
|
import com.swmansion.enriched.markdown.spans.ImageSpan
|
|
15
|
+
import com.swmansion.enriched.markdown.spans.LinkSpacers
|
|
15
16
|
import com.swmansion.enriched.markdown.styles.StyleConfig
|
|
16
17
|
import com.swmansion.enriched.markdown.utils.common.layout.isLayoutRTL
|
|
17
18
|
import com.swmansion.enriched.markdown.utils.text.conversion.HTMLGenerator
|
|
@@ -32,6 +33,14 @@ data class SelectionMenuConfig(
|
|
|
32
33
|
// Plural templates indexed by image count (0..100). Empty when no pluralLabels
|
|
33
34
|
// are set; counts > 100 use copyImageUrlsLabel (the "other" form).
|
|
34
35
|
val copyImageUrlPluralTemplates: List<String> = emptyList(),
|
|
36
|
+
val copyEnabled: Boolean = true,
|
|
37
|
+
// Copy writes Markdown (plain text) instead of plain + HTML / raw block content.
|
|
38
|
+
val copyFormatMarkdown: Boolean = false,
|
|
39
|
+
// Drop every system-provided item (Select all, Share, text assist, PROCESS_TEXT…).
|
|
40
|
+
val systemItemsNone: Boolean = false,
|
|
41
|
+
val codeBlockCopyButton: Boolean = true,
|
|
42
|
+
// Copy hands the selection to onCopyRequest instead of writing the clipboard.
|
|
43
|
+
val interceptCopy: Boolean = false,
|
|
35
44
|
)
|
|
36
45
|
|
|
37
46
|
/**
|
|
@@ -63,6 +72,9 @@ fun createSelectionActionModeCallback(
|
|
|
63
72
|
onActionModeChanged: (mode: ActionMode?) -> Unit = {},
|
|
64
73
|
onCustomItemPress: (itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit =
|
|
65
74
|
{ _, _, _, _ -> },
|
|
75
|
+
// Receives the intercepted Copy when `interceptCopy` is set. `fullSelection`
|
|
76
|
+
// is whether the selection spans the whole text view.
|
|
77
|
+
onCopyRequest: (markdown: String, text: String, fullSelection: Boolean) -> Unit = { _, _, _ -> },
|
|
66
78
|
): ActionMode.Callback =
|
|
67
79
|
object : ActionMode.Callback {
|
|
68
80
|
override fun onCreateActionMode(
|
|
@@ -92,19 +104,26 @@ fun createSelectionActionModeCallback(
|
|
|
92
104
|
menu.removeGroup(MENU_ITEM_CUSTOM_GROUP)
|
|
93
105
|
|
|
94
106
|
val selectionMenuConfig = getSelectionMenuConfig()
|
|
107
|
+
val hasSelection = textView.selectionStart >= 0 && textView.selectionEnd > textView.selectionStart
|
|
95
108
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
109
|
+
if (selectionMenuConfig.systemItemsNone) {
|
|
110
|
+
// Editor re-adds Select all and the text-assist items before calling
|
|
111
|
+
// us on every prepare, so clear on each pass and put back only Copy.
|
|
112
|
+
// Its click is still intercepted in onActionItemClicked.
|
|
113
|
+
menu.clear()
|
|
114
|
+
if (selectionMenuConfig.copyEnabled && hasSelection) {
|
|
115
|
+
menu.add(Menu.NONE, android.R.id.copy, Menu.NONE, selectionMenuConfig.copyLabel)
|
|
116
|
+
}
|
|
117
|
+
} else if (!selectionMenuConfig.copyEnabled) {
|
|
118
|
+
menu.removeItem(android.R.id.copy)
|
|
119
|
+
} else if (selectionMenuConfig.copyLabel.isNotEmpty()) {
|
|
120
|
+
// The system Copy item is added by the platform; relabel it so it
|
|
121
|
+
// matches the rest of the localized menu (iOS rebuilds the menu so
|
|
122
|
+
// gets this for free; Android only intercepts the click).
|
|
100
123
|
menu.findItem(android.R.id.copy)?.title = selectionMenuConfig.copyLabel
|
|
101
124
|
}
|
|
102
125
|
|
|
103
|
-
if (
|
|
104
|
-
selectionMenuConfig.copyAsMarkdown &&
|
|
105
|
-
textView.selectionStart >= 0 &&
|
|
106
|
-
textView.selectionEnd > textView.selectionStart
|
|
107
|
-
) {
|
|
126
|
+
if (selectionMenuConfig.copyAsMarkdown && hasSelection) {
|
|
108
127
|
menu.add(
|
|
109
128
|
Menu.NONE,
|
|
110
129
|
MENU_ITEM_COPY_MARKDOWN,
|
|
@@ -113,7 +132,7 @@ fun createSelectionActionModeCallback(
|
|
|
113
132
|
)
|
|
114
133
|
}
|
|
115
134
|
|
|
116
|
-
if (
|
|
135
|
+
if (hasSelection) {
|
|
117
136
|
val customItems = getCustomItemTexts()
|
|
118
137
|
customItems.forEachIndexed { index, text ->
|
|
119
138
|
menu
|
|
@@ -144,7 +163,12 @@ fun createSelectionActionModeCallback(
|
|
|
144
163
|
|
|
145
164
|
when (itemId) {
|
|
146
165
|
android.R.id.copy -> {
|
|
147
|
-
|
|
166
|
+
val config = getSelectionMenuConfig()
|
|
167
|
+
if (config.interceptCopy) {
|
|
168
|
+
textView.requestCopy(onCopyRequest)
|
|
169
|
+
} else if (!config.copyFormatMarkdown || !textView.copyMarkdownToClipboard()) {
|
|
170
|
+
textView.copyWithHTML()
|
|
171
|
+
}
|
|
148
172
|
mode?.finish()
|
|
149
173
|
return true
|
|
150
174
|
}
|
|
@@ -188,7 +212,8 @@ private fun TextView.copyWithHTML() {
|
|
|
188
212
|
if (start < 0 || end < 0 || start >= end) return
|
|
189
213
|
|
|
190
214
|
val spannable = text as? Spannable ?: return
|
|
191
|
-
|
|
215
|
+
// Link spacers only reserve layout space; keep them out of every copied flavor.
|
|
216
|
+
val selectedText = LinkSpacers.strip(spannable.subSequence(start, end))
|
|
192
217
|
val plainText = selectedText.toString()
|
|
193
218
|
|
|
194
219
|
val styleConfig =
|
|
@@ -214,10 +239,24 @@ private fun TextView.copyWithHTML() {
|
|
|
214
239
|
}
|
|
215
240
|
}
|
|
216
241
|
|
|
217
|
-
|
|
218
|
-
|
|
242
|
+
/** Hands the selection to [onCopyRequest] without touching the clipboard. */
|
|
243
|
+
private fun TextView.requestCopy(onCopyRequest: (markdown: String, text: String, fullSelection: Boolean) -> Unit) {
|
|
244
|
+
val start = selectionStart
|
|
245
|
+
val end = selectionEnd
|
|
246
|
+
if (start < 0 || end < 0 || start >= end) return
|
|
247
|
+
|
|
248
|
+
val selectedText = LinkSpacers.strip(text.subSequence(start, end)).toString()
|
|
249
|
+
val markdown = MarkdownExtractor.getMarkdownForSelection(this) ?: selectedText
|
|
250
|
+
// Same threshold MarkdownExtractor uses to return the original markdown.
|
|
251
|
+
onCopyRequest(markdown, selectedText, start == 0 && end >= text.length - 1)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Returns false when the selection has no Markdown to copy. */
|
|
255
|
+
private fun TextView.copyMarkdownToClipboard(): Boolean {
|
|
256
|
+
val markdown = MarkdownExtractor.getMarkdownForSelection(this) ?: return false
|
|
219
257
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
220
258
|
clipboard.setPrimaryClip(ClipData.newPlainText("Markdown", markdown))
|
|
259
|
+
return true
|
|
221
260
|
}
|
|
222
261
|
|
|
223
262
|
/** Returns remote image URLs (http/https only) from the current selection. */
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.utils.text.view
|
|
2
|
+
|
|
3
|
+
import android.text.Layout
|
|
4
|
+
import android.text.Selection
|
|
5
|
+
import android.text.Spannable
|
|
6
|
+
import android.view.View
|
|
7
|
+
import android.widget.TextView
|
|
8
|
+
import com.facebook.react.uimanager.PixelUtil
|
|
9
|
+
import com.swmansion.enriched.markdown.spans.LinkSpacers
|
|
10
|
+
import com.swmansion.enriched.markdown.utils.text.conversion.MarkdownExtractor
|
|
11
|
+
import kotlin.math.max
|
|
12
|
+
import kotlin.math.min
|
|
13
|
+
|
|
14
|
+
/** A rect in dp, relative to the reporting view. */
|
|
15
|
+
data class SelectionRect(
|
|
16
|
+
val x: Float,
|
|
17
|
+
val y: Float,
|
|
18
|
+
val width: Float,
|
|
19
|
+
val height: Float,
|
|
20
|
+
) {
|
|
21
|
+
companion object {
|
|
22
|
+
val ZERO = SelectionRect(0f, 0f, 0f, 0f)
|
|
23
|
+
|
|
24
|
+
/** Converts a px rect (already in the reporting view's coordinates) to dp. */
|
|
25
|
+
fun fromPx(
|
|
26
|
+
left: Float,
|
|
27
|
+
top: Float,
|
|
28
|
+
right: Float,
|
|
29
|
+
bottom: Float,
|
|
30
|
+
) = SelectionRect(
|
|
31
|
+
PixelUtil.toDIPFromPixel(min(left, right)),
|
|
32
|
+
PixelUtil.toDIPFromPixel(min(top, bottom)),
|
|
33
|
+
PixelUtil.toDIPFromPixel(kotlin.math.abs(right - left)),
|
|
34
|
+
PixelUtil.toDIPFromPixel(kotlin.math.abs(bottom - top)),
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Payload of `onSelectionChange`. Compared structurally for deduplication. */
|
|
40
|
+
data class SelectionSnapshot(
|
|
41
|
+
val active: Boolean,
|
|
42
|
+
val source: String,
|
|
43
|
+
val text: String,
|
|
44
|
+
val markdown: String,
|
|
45
|
+
val whole: Boolean,
|
|
46
|
+
val rect: SelectionRect,
|
|
47
|
+
val startRect: SelectionRect,
|
|
48
|
+
val endRect: SelectionRect,
|
|
49
|
+
// Not tracked on Android: the selection is reported continuously while a handle is dragged.
|
|
50
|
+
val adjusting: Boolean = false,
|
|
51
|
+
) {
|
|
52
|
+
companion object {
|
|
53
|
+
val INACTIVE =
|
|
54
|
+
SelectionSnapshot(false, "selection", "", "", false, SelectionRect.ZERO, SelectionRect.ZERO, SelectionRect.ZERO)
|
|
55
|
+
|
|
56
|
+
/** A long-pressed block: `whole`, and every rect is the block's frame. */
|
|
57
|
+
fun block(
|
|
58
|
+
source: String,
|
|
59
|
+
markdown: String,
|
|
60
|
+
text: String,
|
|
61
|
+
rect: SelectionRect,
|
|
62
|
+
) = SelectionSnapshot(true, source, text, markdown, true, rect, rect, rect)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A text view's current selection, before the owner applies its `whole` rule. */
|
|
67
|
+
data class TextSelection(
|
|
68
|
+
val text: String,
|
|
69
|
+
val markdown: String,
|
|
70
|
+
// Same threshold MarkdownExtractor uses to return the original markdown.
|
|
71
|
+
val fullSelection: Boolean,
|
|
72
|
+
val rect: SelectionRect,
|
|
73
|
+
val startRect: SelectionRect,
|
|
74
|
+
val endRect: SelectionRect,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Reads the selection of this text view with its rects offset by [offsetXPx]/[offsetYPx]
|
|
79
|
+
* (a segment's position inside its container). `rect` spans the full content width when the
|
|
80
|
+
* selection covers several lines; `startRect`/`endRect` cover the selected part of the first
|
|
81
|
+
* and last line. Returns null when nothing is selected.
|
|
82
|
+
*/
|
|
83
|
+
fun TextView.currentTextSelection(
|
|
84
|
+
offsetXPx: Int = 0,
|
|
85
|
+
offsetYPx: Int = 0,
|
|
86
|
+
): TextSelection? {
|
|
87
|
+
val start = selectionStart
|
|
88
|
+
val end = selectionEnd
|
|
89
|
+
if (start < 0 || end <= start) return null
|
|
90
|
+
val layout = layout ?: return null
|
|
91
|
+
val content = text ?: return null
|
|
92
|
+
|
|
93
|
+
val selectedText = LinkSpacers.strip(content.subSequence(start, end)).toString()
|
|
94
|
+
val markdown = MarkdownExtractor.getMarkdownForSelection(this) ?: selectedText
|
|
95
|
+
|
|
96
|
+
val originX = (offsetXPx + totalPaddingLeft - scrollX).toFloat()
|
|
97
|
+
val originY = (offsetYPx + totalPaddingTop - scrollY).toFloat()
|
|
98
|
+
|
|
99
|
+
val startLine = layout.getLineForOffset(start)
|
|
100
|
+
// An end at a line break belongs to the line it closes, not the next one.
|
|
101
|
+
val endLine = layout.getLineForOffset(max(start, end - 1))
|
|
102
|
+
val startX = layout.getPrimaryHorizontal(start)
|
|
103
|
+
val endX = lineEndHorizontal(layout, endLine, end)
|
|
104
|
+
|
|
105
|
+
fun lineRect(
|
|
106
|
+
line: Int,
|
|
107
|
+
left: Float,
|
|
108
|
+
right: Float,
|
|
109
|
+
) = SelectionRect.fromPx(
|
|
110
|
+
left + originX,
|
|
111
|
+
layout.getLineTop(line) + originY,
|
|
112
|
+
right + originX,
|
|
113
|
+
layout.getLineBottom(line) + originY,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
val startRect: SelectionRect
|
|
117
|
+
val endRect: SelectionRect
|
|
118
|
+
val rect: SelectionRect
|
|
119
|
+
if (startLine == endLine) {
|
|
120
|
+
startRect = lineRect(startLine, startX, endX)
|
|
121
|
+
endRect = startRect
|
|
122
|
+
rect = startRect
|
|
123
|
+
} else {
|
|
124
|
+
startRect = lineRect(startLine, startX, layout.getLineRight(startLine))
|
|
125
|
+
endRect = lineRect(endLine, layout.getLineLeft(endLine), endX)
|
|
126
|
+
rect =
|
|
127
|
+
SelectionRect.fromPx(
|
|
128
|
+
originX,
|
|
129
|
+
layout.getLineTop(startLine) + originY,
|
|
130
|
+
layout.width + originX,
|
|
131
|
+
layout.getLineBottom(endLine) + originY,
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return TextSelection(
|
|
136
|
+
text = selectedText,
|
|
137
|
+
markdown = markdown,
|
|
138
|
+
fullSelection = start == 0 && end >= content.length - 1,
|
|
139
|
+
rect = rect,
|
|
140
|
+
startRect = startRect,
|
|
141
|
+
endRect = endRect,
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Clears this text view's selection, which also ends its selection ActionMode. */
|
|
146
|
+
fun TextView.clearTextSelection() {
|
|
147
|
+
val spannable = text as? Spannable ?: return
|
|
148
|
+
Selection.removeSelection(spannable)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* x of [offset] on [line]. At a line's end offset `getPrimaryHorizontal` reports the start
|
|
153
|
+
* of the next line, so a selection ending there uses the line's right edge.
|
|
154
|
+
*/
|
|
155
|
+
private fun lineEndHorizontal(
|
|
156
|
+
layout: Layout,
|
|
157
|
+
line: Int,
|
|
158
|
+
offset: Int,
|
|
159
|
+
): Float =
|
|
160
|
+
if (offset >= layout.getLineEnd(line) && line < layout.lineCount - 1) {
|
|
161
|
+
layout.getLineRight(line)
|
|
162
|
+
} else {
|
|
163
|
+
layout.getPrimaryHorizontal(offset)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Coalesces selection changes into at most one `onSelectionChange` per frame. The payload is
|
|
168
|
+
* read when the frame fires, so the latest state always reaches JS; a cleared selection is
|
|
169
|
+
* reported right away and drops any pending update. Identical payloads are sent once, so the
|
|
170
|
+
* `setText` passes of streaming don't repeat `active: false`.
|
|
171
|
+
*/
|
|
172
|
+
class SelectionChangeReporter(
|
|
173
|
+
private val view: View,
|
|
174
|
+
private val snapshot: () -> SelectionSnapshot,
|
|
175
|
+
private val emit: (SelectionSnapshot) -> Unit,
|
|
176
|
+
) {
|
|
177
|
+
var enabled = false
|
|
178
|
+
set(value) {
|
|
179
|
+
if (field == value) return
|
|
180
|
+
field = value
|
|
181
|
+
cancel()
|
|
182
|
+
lastReported = SelectionSnapshot.INACTIVE
|
|
183
|
+
if (value) schedule()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private var lastReported = SelectionSnapshot.INACTIVE
|
|
187
|
+
private var pending = false
|
|
188
|
+
private val flushRunnable =
|
|
189
|
+
Runnable {
|
|
190
|
+
pending = false
|
|
191
|
+
report()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Reports the current state on the next frame. */
|
|
195
|
+
fun schedule() {
|
|
196
|
+
if (!enabled || pending) return
|
|
197
|
+
pending = true
|
|
198
|
+
view.postOnAnimation(flushRunnable)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
fun onSelectionChanged(hasSelection: Boolean) {
|
|
202
|
+
if (hasSelection) schedule() else flushNow()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Reports the current state right away, dropping any pending frame. */
|
|
206
|
+
fun flushNow() {
|
|
207
|
+
cancel()
|
|
208
|
+
report()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Makes the next report go out even if it equals the last one — a block long-pressed
|
|
213
|
+
* again is a new request for the app's menu, not a repeat.
|
|
214
|
+
*/
|
|
215
|
+
fun forgetLastReported() {
|
|
216
|
+
lastReported = SelectionSnapshot.INACTIVE
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
fun cancel() {
|
|
220
|
+
if (!pending) return
|
|
221
|
+
pending = false
|
|
222
|
+
view.removeCallbacks(flushRunnable)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private fun report() {
|
|
226
|
+
if (!enabled) return
|
|
227
|
+
val current = snapshot()
|
|
228
|
+
if (current == lastReported) return
|
|
229
|
+
lastReported = current
|
|
230
|
+
emit(current)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.views
|
|
2
|
+
|
|
3
|
+
import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig
|
|
4
|
+
|
|
5
|
+
/** Receives an intercepted block Copy: the block's Markdown and its raw content. */
|
|
6
|
+
typealias BlockCopyRequest = (markdown: String, text: String) -> Unit
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Selection-menu options that table, math and code block views need for their
|
|
10
|
+
* own long-press menus. Every default keeps the legacy behavior, so a block view
|
|
11
|
+
* that has not received options yet behaves exactly as before.
|
|
12
|
+
*/
|
|
13
|
+
data class BlockMenuOptions(
|
|
14
|
+
val copyHidden: Boolean = false,
|
|
15
|
+
// The Copy item writes the block's Markdown instead of its raw content.
|
|
16
|
+
val copyWritesMarkdown: Boolean = false,
|
|
17
|
+
val copyAsMarkdownHidden: Boolean = false,
|
|
18
|
+
// Hides the copy button in the code block header.
|
|
19
|
+
val copyButtonHidden: Boolean = false,
|
|
20
|
+
// Set when `onCopy` is on: the Copy item hands the content here instead of
|
|
21
|
+
// writing the clipboard. The host binds it to the block's source.
|
|
22
|
+
val onCopyRequest: BlockCopyRequest? = null,
|
|
23
|
+
) {
|
|
24
|
+
val hasItems: Boolean get() = !copyHidden || !copyAsMarkdownHidden
|
|
25
|
+
|
|
26
|
+
companion object {
|
|
27
|
+
fun from(
|
|
28
|
+
config: SelectionMenuConfig,
|
|
29
|
+
onCopyRequest: BlockCopyRequest? = null,
|
|
30
|
+
) = BlockMenuOptions(
|
|
31
|
+
copyHidden = !config.copyEnabled,
|
|
32
|
+
copyWritesMarkdown = config.copyFormatMarkdown,
|
|
33
|
+
copyAsMarkdownHidden = !config.copyAsMarkdown,
|
|
34
|
+
copyButtonHidden = !config.codeBlockCopyButton,
|
|
35
|
+
onCopyRequest = onCopyRequest.takeIf { config.interceptCopy },
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Adds the Copy / Copy as Markdown items shared by the table, math and code block menus. */
|
|
41
|
+
fun ContextMenuPopup.Builder.blockCopyItems(
|
|
42
|
+
options: BlockMenuOptions,
|
|
43
|
+
copyLabel: String,
|
|
44
|
+
copyAsMarkdownLabel: String,
|
|
45
|
+
// The block's Markdown and raw content, read only for an intercepted Copy.
|
|
46
|
+
markdown: () -> String,
|
|
47
|
+
rawText: () -> String,
|
|
48
|
+
copyRaw: () -> Unit,
|
|
49
|
+
copyMarkdown: () -> Unit,
|
|
50
|
+
) {
|
|
51
|
+
if (!options.copyHidden) {
|
|
52
|
+
val onCopyRequest = options.onCopyRequest
|
|
53
|
+
val onCopy =
|
|
54
|
+
when {
|
|
55
|
+
onCopyRequest != null -> { -> onCopyRequest(markdown(), rawText()) }
|
|
56
|
+
options.copyWritesMarkdown -> copyMarkdown
|
|
57
|
+
else -> copyRaw
|
|
58
|
+
}
|
|
59
|
+
item(ContextMenuPopup.Icon.COPY, copyLabel, onCopy)
|
|
60
|
+
}
|
|
61
|
+
if (!options.copyAsMarkdownHidden) {
|
|
62
|
+
item(ContextMenuPopup.Icon.DOCUMENT, copyAsMarkdownLabel, copyMarkdown)
|
|
63
|
+
}
|
|
64
|
+
}
|
package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt
CHANGED
|
@@ -70,6 +70,19 @@ class CodeBlockContainerView(
|
|
|
70
70
|
// control, not a context menu, and stays.
|
|
71
71
|
var contextMenuHidden: Boolean = false
|
|
72
72
|
|
|
73
|
+
// Called instead of the menu on long press while contextMenuHidden is on, so
|
|
74
|
+
// the owner can report the block (onSelectionChange) and show its own UI.
|
|
75
|
+
var onMenuSuppressed: ((markdown: String, text: String) -> Unit)? = null
|
|
76
|
+
|
|
77
|
+
// Copy / Copy as Markdown visibility, Copy format and the header copy button.
|
|
78
|
+
// The header button always copies the raw code.
|
|
79
|
+
var menuOptions: BlockMenuOptions = BlockMenuOptions()
|
|
80
|
+
set(value) {
|
|
81
|
+
field = value
|
|
82
|
+
// Hiding the button leaves the header (and its height) untouched.
|
|
83
|
+
copyButton.visibility = if (value.copyButtonHidden) View.GONE else View.VISIBLE
|
|
84
|
+
}
|
|
85
|
+
|
|
73
86
|
var onCopyPress: ((code: String, language: String) -> Unit)? = null
|
|
74
87
|
|
|
75
88
|
private var code: String = ""
|
|
@@ -250,10 +263,23 @@ class CodeBlockContainerView(
|
|
|
250
263
|
// the event when there is one (a pending block has no menu yet, and a hidden
|
|
251
264
|
// one never does).
|
|
252
265
|
private fun showContextMenu(anchor: View): Boolean {
|
|
253
|
-
if (pending
|
|
266
|
+
if (pending) return false
|
|
267
|
+
if (contextMenuHidden) {
|
|
268
|
+
val onSuppressed = onMenuSuppressed ?: return false
|
|
269
|
+
onSuppressed(CodeBlockNode.fencedMarkdown(code, language, fenceChar), code)
|
|
270
|
+
return true
|
|
271
|
+
}
|
|
272
|
+
if (!menuOptions.hasItems) return false
|
|
254
273
|
ContextMenuPopup.show(anchor, this) {
|
|
255
|
-
|
|
256
|
-
|
|
274
|
+
blockCopyItems(
|
|
275
|
+
menuOptions,
|
|
276
|
+
copyLabel,
|
|
277
|
+
copyAsMarkdownLabel,
|
|
278
|
+
markdown = { CodeBlockNode.fencedMarkdown(code, language, fenceChar) },
|
|
279
|
+
rawText = { code },
|
|
280
|
+
copyRaw = ::copyCode,
|
|
281
|
+
copyMarkdown = ::copyFencedMarkdown,
|
|
282
|
+
)
|
|
257
283
|
}
|
|
258
284
|
return true
|
|
259
285
|
}
|
|
@@ -24,6 +24,7 @@ import com.swmansion.enriched.markdown.accessibility.AccessibilityLabels
|
|
|
24
24
|
import com.swmansion.enriched.markdown.parser.MarkdownASTNode
|
|
25
25
|
import com.swmansion.enriched.markdown.parser.MarkdownASTNode.NodeType
|
|
26
26
|
import com.swmansion.enriched.markdown.renderer.Renderer
|
|
27
|
+
import com.swmansion.enriched.markdown.spans.LinkChipBackgroundSpan
|
|
27
28
|
import com.swmansion.enriched.markdown.styles.StyleConfig
|
|
28
29
|
import com.swmansion.enriched.markdown.styles.TableStyle
|
|
29
30
|
import com.swmansion.enriched.markdown.utils.common.layout.isLayoutRTL
|
|
@@ -64,6 +65,11 @@ class TableContainerView(
|
|
|
64
65
|
var copyLabel: String = ""
|
|
65
66
|
var copyAsMarkdownLabel: String = ""
|
|
66
67
|
var contextMenuHidden: Boolean = false
|
|
68
|
+
var menuOptions: BlockMenuOptions = BlockMenuOptions()
|
|
69
|
+
|
|
70
|
+
// Called instead of the menu on long press while contextMenuHidden is on, so
|
|
71
|
+
// the owner can report the block (onSelectionChange) and show its own UI.
|
|
72
|
+
var onMenuSuppressed: ((markdown: String, text: String) -> Unit)? = null
|
|
67
73
|
|
|
68
74
|
private val scrollView =
|
|
69
75
|
HorizontalScrollView(context).apply {
|
|
@@ -80,6 +86,19 @@ class TableContainerView(
|
|
|
80
86
|
|
|
81
87
|
val rowCount: Int get() = rows.size
|
|
82
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Number of links across all cells. Tables are not reported by `onLinkLayout`, but their links
|
|
91
|
+
* still count so later link indices stay in document order.
|
|
92
|
+
*/
|
|
93
|
+
val linkCount: Int
|
|
94
|
+
get() =
|
|
95
|
+
rows.sumOf { row ->
|
|
96
|
+
row.sumOf { cell ->
|
|
97
|
+
// LinkRenderer attaches one LinkChipBackgroundSpan per link.
|
|
98
|
+
cell.attributedText.getSpans(0, cell.attributedText.length, LinkChipBackgroundSpan::class.java).size
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
83
102
|
fun animateNewRows(
|
|
84
103
|
previousRowCount: Int,
|
|
85
104
|
durationMs: Long,
|
|
@@ -363,11 +382,14 @@ class TableContainerView(
|
|
|
363
382
|
}
|
|
364
383
|
|
|
365
384
|
private fun showContextMenu(anchor: View) {
|
|
366
|
-
if (contextMenuHidden)
|
|
385
|
+
if (contextMenuHidden) {
|
|
386
|
+
onMenuSuppressed?.invoke(tableMarkdown, buildPlainText())
|
|
387
|
+
return
|
|
388
|
+
}
|
|
367
389
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
368
390
|
ContextMenuPopup.show(anchor, this) {
|
|
369
|
-
|
|
370
|
-
val plainText =
|
|
391
|
+
blockCopyItems(menuOptions, copyLabel, copyAsMarkdownLabel, markdown = { tableMarkdown }, rawText = ::buildPlainText, copyRaw = {
|
|
392
|
+
val plainText = buildPlainText()
|
|
371
393
|
if (plainText.isNotEmpty()) {
|
|
372
394
|
val displayMetrics = context.resources.displayMetrics
|
|
373
395
|
val tableRows =
|
|
@@ -377,13 +399,14 @@ class TableContainerView(
|
|
|
377
399
|
val html = HTMLGenerator.generateTableHTML(tableRows, styleConfig, displayMetrics.scaledDensity, displayMetrics.density)
|
|
378
400
|
clipboard.setPrimaryClip(ClipData.newHtmlText("Table", plainText, html))
|
|
379
401
|
}
|
|
380
|
-
}
|
|
381
|
-
item(ContextMenuPopup.Icon.DOCUMENT, copyAsMarkdownLabel) {
|
|
402
|
+
}, copyMarkdown = {
|
|
382
403
|
if (tableMarkdown.isNotEmpty()) clipboard.setPrimaryClip(ClipData.newPlainText("Table", tableMarkdown))
|
|
383
|
-
}
|
|
404
|
+
})
|
|
384
405
|
}
|
|
385
406
|
}
|
|
386
407
|
|
|
408
|
+
private fun buildPlainText(): String = rows.joinToString("\n") { row -> row.joinToString("\t") { it.plainText } }
|
|
409
|
+
|
|
387
410
|
private fun buildMarkdownFromRows(): String =
|
|
388
411
|
rows.joinToString("") { row ->
|
|
389
412
|
val line = "| ${row.joinToString(" | ") { it.markdownText }} |\n"
|
|
@@ -483,7 +506,7 @@ class TableContainerView(
|
|
|
483
506
|
.apply { configure(config, context) }
|
|
484
507
|
.renderDocument(MarkdownASTNode(NodeType.Document, children = listOf(paragraph)), null, null)
|
|
485
508
|
}
|
|
486
|
-
styledText.replaceMathSpansWithPlaceholders(context, config.inlineMathStyle.
|
|
509
|
+
styledText.replaceMathSpansWithPlaceholders(context, config.inlineMathStyle.boxPaddingVertical)
|
|
487
510
|
if (isHeader && styledText.isNotEmpty()) {
|
|
488
511
|
styledText.setSpan(HeaderTypefaceSpan(headerTypeface), 0, styledText.length, 33)
|
|
489
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)
|