react-native-enriched-markdown-scaffold 0.6.0-scaffold.8 → 0.7.4-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/ReactNativeEnrichedMarkdown.podspec +62 -11
- package/android/build.gradle +5 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +3 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +1 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerDelegate.java +12 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerInterface.java +4 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +3 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +1 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +25 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +793 -0
- package/android/src/main/baseline-prof.txt +75 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +62 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +10 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +10 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +9 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +10 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +6 -4
- package/android/src/main/java/com/swmansion/enriched/markdown/accessibility/AccessibilityLabels.kt +15 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/accessibility/MarkdownAccessibilityHelper.kt +44 -7
- package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt +72 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +110 -12
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/BlockSyntaxSentinel.kt +69 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/FormattingStore.kt +19 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputParser.kt +84 -6
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputRemend.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputLayoutManager.kt +30 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputMeasurementStore.kt +13 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/input/model/MathChipStyle.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/toolbar/InputContextMenu.kt +54 -13
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +4 -4
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/HeadingSpan.kt +8 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +27 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineMathStyle.kt +5 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/MathFontWeight.kt +25 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/MathStyle.kt +12 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +37 -7
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/MarkdownStyleParser.kt +10 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/extensions/SpannableExtensions.kt +12 -4
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkEvents.kt +22 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +36 -7
- package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +74 -4
- package/android/src/main/jni/CMakeLists.txt +15 -1
- package/android/src/math/java/com/swmansion/enriched/markdown/renderer/MathInlineRenderer.kt +2 -0
- package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathChipSpan.kt +1 -1
- package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathInlinePlaceholderSpan.kt +7 -3
- package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathInlineSpan.kt +8 -5
- package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathTypeface.kt +30 -0
- package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +27 -3
- package/app.plugin.js +2 -1
- package/cpp/parser/MD4CParser.cpp +1 -1
- package/cpp/wasm/build.sh +9 -9
- package/docs/ACCESSIBILITY.md +172 -0
- package/docs/API_REFERENCE.md +957 -0
- package/docs/COPY_OPTIONS.md +127 -0
- package/docs/ELEMENTS_STRUCTURE.md +112 -0
- package/docs/IMAGE_CACHING.md +25 -0
- package/docs/INPUT.md +281 -0
- package/docs/LATEX_MATH.md +198 -0
- package/docs/MACOS.md +19 -0
- package/docs/MARKDOWN_STREAMING.md +36 -0
- package/docs/MENTIONS.md +97 -0
- package/docs/RTL.md +82 -0
- package/docs/STYLES.md +522 -0
- package/docs/TEXT.md +110 -0
- package/docs/WEB.md +49 -0
- package/ios/EnrichedMarkdown.mm +126 -37
- package/ios/EnrichedMarkdownText.mm +84 -16
- package/ios/attachments/ENRMMathInlineAttachment+macOS.m +5 -1
- package/ios/attachments/ENRMMathInlineAttachment.h +3 -0
- package/ios/attachments/ENRMMathInlineAttachment.m +2 -1
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +25 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +793 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +15 -0
- package/ios/input/ENRMBlockSyntaxSentinel.h +25 -0
- package/ios/input/ENRMBlockSyntaxSentinel.mm +80 -0
- package/ios/input/ENRMFormattingStore.h +5 -0
- package/ios/input/ENRMFormattingStore.mm +34 -0
- package/ios/input/ENRMInputFormatter.h +4 -0
- package/ios/input/ENRMInputFormatter.mm +2 -0
- package/ios/input/ENRMInputParser.h +5 -0
- package/ios/input/ENRMInputParser.mm +79 -6
- package/ios/input/ENRMInputTextView.h +2 -0
- package/ios/input/ENRMInputTextView.mm +8 -17
- package/ios/input/ENRMMarkdownSerializer.mm +1 -1
- package/ios/input/EnrichedMarkdownTextInput+ContextMenu.mm +71 -53
- package/ios/input/EnrichedMarkdownTextInput+Internal.h +26 -0
- package/ios/input/EnrichedMarkdownTextInput.h +1 -0
- package/ios/input/EnrichedMarkdownTextInput.mm +249 -31
- package/ios/input/InputStylePropsUtils.h +13 -0
- package/ios/input/styles/ENRMBoldStyleHandler.mm +3 -2
- package/ios/input/styles/ENRMItalicStyleHandler.mm +3 -2
- package/ios/internals/MeasurementCache.h +1 -0
- package/ios/math/ENRMRaTeXBridge.swift +52 -3
- package/ios/parser/ENRMMarkdownParser.h +8 -0
- package/ios/parser/ENRMMarkdownParser.mm +11 -0
- package/ios/renderer/AttributedRenderer.m +51 -6
- package/ios/renderer/ENRMMathInlineRenderer.m +8 -1
- package/ios/renderer/ListItemRenderer.m +8 -5
- package/ios/styles/ENRMFontSlot.h +1 -1
- package/ios/styles/StyleConfig.h +6 -0
- package/ios/styles/StyleConfig.mm +36 -0
- package/ios/utils/ENRMAccessibilityLabels.h +36 -0
- package/ios/utils/ENRMAccessibilityLabels.m +5 -0
- package/ios/utils/ENRMMathFontWeight.h +18 -0
- package/ios/utils/EditMenuUtils+macOS.m +4 -5
- package/ios/utils/EditMenuUtils.h +70 -0
- package/ios/utils/EditMenuUtils.m +14 -11
- package/ios/utils/LastElementUtils.h +39 -2
- package/ios/utils/MarkdownAccessibilityElementBuilder.h +18 -16
- package/ios/utils/MarkdownAccessibilityElementBuilder.m +64 -32
- package/ios/utils/ParagraphStyleUtils.m +21 -2
- package/ios/utils/StylePropsUtils.h +21 -0
- package/ios/views/ENRMMathContainerView.h +8 -0
- package/ios/views/ENRMMathContainerView.m +10 -6
- package/ios/views/ENRMTableIOSGridView.h +40 -0
- package/ios/views/ENRMTableIOSGridView.m +216 -0
- package/ios/views/EnrichedMarkdownInternalText.h +2 -0
- package/ios/views/EnrichedMarkdownInternalText.m +2 -0
- package/ios/views/TableContainerView.h +8 -0
- package/ios/views/TableContainerView.m +72 -128
- package/lib/module/EnrichedMarkdownNativeComponent.ts +40 -0
- package/lib/module/EnrichedMarkdownTextInput.js +50 -1
- package/lib/module/EnrichedMarkdownTextInput.js.map +1 -1
- package/lib/module/EnrichedMarkdownTextInputNativeComponent.ts +47 -0
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +40 -0
- package/lib/module/accessibilityLabelDefaults.js +64 -0
- package/lib/module/accessibilityLabelDefaults.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/EnrichedMarkdownText.js +72 -4
- package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
- package/lib/module/normalizeMarkdownStyle.js +5 -2
- package/lib/module/normalizeMarkdownStyle.js.map +1 -1
- package/lib/module/normalizeMarkdownStyle.web.js +5 -2
- package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
- package/lib/module/normalizeMarkdownTextInputStyle.js +7 -3
- package/lib/module/normalizeMarkdownTextInputStyle.js.map +1 -1
- package/lib/module/normalizeMenuItem.js +38 -0
- package/lib/module/normalizeMenuItem.js.map +1 -0
- package/lib/module/types/AccessibilityLabels.js +2 -0
- package/lib/module/types/AccessibilityLabels.js.map +1 -0
- package/lib/module/web/globalStyles.js +8 -1
- package/lib/module/web/globalStyles.js.map +1 -1
- package/lib/module/web/styles.js +3 -1
- package/lib/module/web/styles.js.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +37 -0
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts +57 -1
- package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts +42 -0
- package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +37 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/accessibilityLabelDefaults.d.ts +16 -0
- package/lib/typescript/src/accessibilityLabelDefaults.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +3 -3
- 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/normalizeMarkdownTextInputStyle.d.ts +1 -0
- package/lib/typescript/src/normalizeMarkdownTextInputStyle.d.ts.map +1 -1
- package/lib/typescript/src/normalizeMenuItem.d.ts +7 -0
- package/lib/typescript/src/normalizeMenuItem.d.ts.map +1 -0
- package/lib/typescript/src/types/AccessibilityLabels.d.ts +114 -0
- package/lib/typescript/src/types/AccessibilityLabels.d.ts.map +1 -0
- package/lib/typescript/src/types/MarkdownStyle.d.ts +32 -0
- package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +3 -0
- package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownTextProps.d.ts +78 -9
- package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
- package/lib/typescript/src/web/styles.d.ts.map +1 -1
- package/package.json +4 -2
- package/plugin/build/withAndroidMath.js +19 -0
- package/plugin/build/withIosMath.js +28 -0
- package/plugin/build/withReactNativeEnrichedMarkdown.js +11 -0
- package/plugin/tsconfig.build.json +16 -0
- package/src/EnrichedMarkdownNativeComponent.ts +40 -0
- package/src/EnrichedMarkdownTextInput.tsx +121 -1
- package/src/EnrichedMarkdownTextInputNativeComponent.ts +47 -0
- package/src/EnrichedMarkdownTextNativeComponent.ts +40 -0
- package/src/accessibilityLabelDefaults.ts +83 -0
- package/src/index.tsx +11 -0
- package/src/native/EnrichedMarkdownText.tsx +120 -6
- package/src/normalizeMarkdownStyle.ts +6 -1
- package/src/normalizeMarkdownStyle.web.ts +6 -1
- package/src/normalizeMarkdownTextInputStyle.ts +5 -0
- package/src/normalizeMenuItem.ts +52 -0
- package/src/types/AccessibilityLabels.ts +118 -0
- package/src/types/MarkdownStyle.ts +32 -0
- package/src/types/MarkdownStyleInternal.ts +3 -0
- package/src/types/MarkdownTextProps.ts +74 -9
- package/src/web/globalStyles.ts +7 -0
- package/src/web/styles.ts +5 -1
- package/lib/module/plugin/withAndroidMath.js +0 -20
- package/lib/module/plugin/withAndroidMath.js.map +0 -1
- package/lib/module/plugin/withIosMath.js +0 -23
- package/lib/module/plugin/withIosMath.js.map +0 -1
- package/lib/module/plugin/withReactNativeEnrichedMarkdown.js +0 -16
- package/lib/module/plugin/withReactNativeEnrichedMarkdown.js.map +0 -1
- package/lib/typescript/src/plugin/withAndroidMath.d.ts +0 -5
- package/lib/typescript/src/plugin/withAndroidMath.d.ts.map +0 -1
- package/lib/typescript/src/plugin/withIosMath.d.ts +0 -5
- package/lib/typescript/src/plugin/withIosMath.d.ts.map +0 -1
- package/lib/typescript/src/plugin/withReactNativeEnrichedMarkdown.d.ts +0 -6
- package/lib/typescript/src/plugin/withReactNativeEnrichedMarkdown.d.ts.map +0 -1
- /package/{src/plugin → plugin/src}/withAndroidMath.ts +0 -0
- /package/{src/plugin → plugin/src}/withIosMath.ts +0 -0
- /package/{src/plugin → plugin/src}/withReactNativeEnrichedMarkdown.ts +0 -0
|
@@ -32,6 +32,8 @@ import com.swmansion.enriched.markdown.input.events.OnRequestMarkdownResultEvent
|
|
|
32
32
|
import com.swmansion.enriched.markdown.input.events.OnStartMentionEvent
|
|
33
33
|
import com.swmansion.enriched.markdown.input.layout.InputMeasurementStore
|
|
34
34
|
import com.swmansion.enriched.markdown.input.model.StyleType
|
|
35
|
+
import com.swmansion.enriched.markdown.input.toolbar.FormatMenuConfig
|
|
36
|
+
import com.swmansion.enriched.markdown.input.toolbar.InputSelectionMenuConfig
|
|
35
37
|
import com.swmansion.enriched.markdown.utils.input.BorderPropsApplicator
|
|
36
38
|
import com.swmansion.enriched.markdown.utils.input.MarkdownStyleParser
|
|
37
39
|
|
|
@@ -55,6 +57,13 @@ class EnrichedMarkdownTextInputManager :
|
|
|
55
57
|
stateWrapper: StateWrapper?,
|
|
56
58
|
): Any? {
|
|
57
59
|
view.stateWrapper = stateWrapper
|
|
60
|
+
// Fabric applies props before it hands over the state wrapper, so every layout
|
|
61
|
+
// invalidation raised during the mount transaction was dropped. Replay it now that we can
|
|
62
|
+
// actually push state — otherwise the shadow node keeps the initialMeasure() estimate,
|
|
63
|
+
// which ignores math chips, until the first keystroke.
|
|
64
|
+
if (stateWrapper != null) {
|
|
65
|
+
view.layoutManager.flushPendingInvalidation()
|
|
66
|
+
}
|
|
58
67
|
return super.updateState(view, props, stateWrapper)
|
|
59
68
|
}
|
|
60
69
|
|
|
@@ -67,6 +76,11 @@ class EnrichedMarkdownTextInputManager :
|
|
|
67
76
|
view.dismissActiveMention()
|
|
68
77
|
super.onDropViewInstance(view)
|
|
69
78
|
view.layoutManager.release()
|
|
79
|
+
// Unreachable while view recycling is off — it needs a `setupViewRecycling()` call from this
|
|
80
|
+
// constructor *and* ReactNativeFeatureFlags.enableViewRecycling(). Kept because the flush
|
|
81
|
+
// above turns a recycled view into a view that pushes state at mount: without this, it would
|
|
82
|
+
// push into the shadow node it was dropped from.
|
|
83
|
+
view.stateWrapper = null
|
|
70
84
|
}
|
|
71
85
|
|
|
72
86
|
override fun measure(
|
|
@@ -156,6 +170,14 @@ class EnrichedMarkdownTextInputManager :
|
|
|
156
170
|
view?.isVerticalScrollBarEnabled = value
|
|
157
171
|
}
|
|
158
172
|
|
|
173
|
+
@ReactProp(name = "blockSyntax", defaultBoolean = true)
|
|
174
|
+
override fun setBlockSyntax(
|
|
175
|
+
view: EnrichedMarkdownTextInputView?,
|
|
176
|
+
value: Boolean,
|
|
177
|
+
) {
|
|
178
|
+
view?.blockSyntax = value
|
|
179
|
+
}
|
|
180
|
+
|
|
159
181
|
@ReactProp(name = "autoCapitalize")
|
|
160
182
|
override fun setAutoCapitalize(
|
|
161
183
|
view: EnrichedMarkdownTextInputView?,
|
|
@@ -266,6 +288,52 @@ class EnrichedMarkdownTextInputManager :
|
|
|
266
288
|
view.setContextMenuItems(items)
|
|
267
289
|
}
|
|
268
290
|
|
|
291
|
+
@ReactProp(name = "selectionMenuConfig")
|
|
292
|
+
override fun setSelectionMenuConfig(
|
|
293
|
+
view: EnrichedMarkdownTextInputView?,
|
|
294
|
+
value: ReadableMap?,
|
|
295
|
+
) {
|
|
296
|
+
if (view == null) return
|
|
297
|
+
view.contextMenu.selectionMenuConfig =
|
|
298
|
+
if (value == null) {
|
|
299
|
+
InputSelectionMenuConfig()
|
|
300
|
+
} else {
|
|
301
|
+
InputSelectionMenuConfig(
|
|
302
|
+
format = value.getBoolean("format"),
|
|
303
|
+
formatLabel = value.getString("formatLabel") ?: "",
|
|
304
|
+
copyAsMarkdown = value.getBoolean("copyAsMarkdown"),
|
|
305
|
+
copyAsMarkdownLabel = value.getString("copyAsMarkdownLabel") ?: "",
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
@ReactProp(name = "formatMenuConfig")
|
|
311
|
+
override fun setFormatMenuConfig(
|
|
312
|
+
view: EnrichedMarkdownTextInputView?,
|
|
313
|
+
value: ReadableMap?,
|
|
314
|
+
) {
|
|
315
|
+
if (view == null) return
|
|
316
|
+
view.contextMenu.formatMenuConfig =
|
|
317
|
+
if (value == null) {
|
|
318
|
+
FormatMenuConfig()
|
|
319
|
+
} else {
|
|
320
|
+
FormatMenuConfig(
|
|
321
|
+
bold = value.getBoolean("bold"),
|
|
322
|
+
boldLabel = value.getString("boldLabel") ?: "",
|
|
323
|
+
italic = value.getBoolean("italic"),
|
|
324
|
+
italicLabel = value.getString("italicLabel") ?: "",
|
|
325
|
+
underline = value.getBoolean("underline"),
|
|
326
|
+
underlineLabel = value.getString("underlineLabel") ?: "",
|
|
327
|
+
strikethrough = value.getBoolean("strikethrough"),
|
|
328
|
+
strikethroughLabel = value.getString("strikethroughLabel") ?: "",
|
|
329
|
+
spoiler = value.getBoolean("spoiler"),
|
|
330
|
+
spoilerLabel = value.getString("spoilerLabel") ?: "",
|
|
331
|
+
link = value.getBoolean("link"),
|
|
332
|
+
linkLabel = value.getString("linkLabel") ?: "",
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
269
337
|
@ReactProp(name = "linkRegex")
|
|
270
338
|
override fun setLinkRegex(
|
|
271
339
|
view: EnrichedMarkdownTextInputView?,
|
|
@@ -445,6 +513,10 @@ class EnrichedMarkdownTextInputManager :
|
|
|
445
513
|
view?.removeMath(start)
|
|
446
514
|
}
|
|
447
515
|
|
|
516
|
+
override fun copyToClipboard(view: EnrichedMarkdownTextInputView?) {
|
|
517
|
+
view?.copyToClipboard()
|
|
518
|
+
}
|
|
519
|
+
|
|
448
520
|
override fun requestMarkdown(
|
|
449
521
|
view: EnrichedMarkdownTextInputView?,
|
|
450
522
|
requestId: Int,
|
package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package com.swmansion.enriched.markdown.input
|
|
2
2
|
|
|
3
|
+
import android.content.ClipData
|
|
4
|
+
import android.content.ClipboardManager
|
|
3
5
|
import android.content.Context
|
|
4
6
|
import android.graphics.BlendMode
|
|
5
7
|
import android.graphics.BlendModeColorFilter
|
|
@@ -44,6 +46,8 @@ import com.swmansion.enriched.markdown.input.toolbar.InputContextMenu
|
|
|
44
46
|
import com.swmansion.enriched.markdown.utils.input.AutoCapitalizeUtils
|
|
45
47
|
import kotlin.math.ceil
|
|
46
48
|
|
|
49
|
+
private fun Char.isLineBreak(): Boolean = this == '\n' || this == '\r' || this == '\u0085' || this == '\u2028' || this == '\u2029'
|
|
50
|
+
|
|
47
51
|
class EnrichedMarkdownTextInputView(
|
|
48
52
|
context: Context,
|
|
49
53
|
) : AppCompatEditText(context) {
|
|
@@ -71,6 +75,7 @@ class EnrichedMarkdownTextInputView(
|
|
|
71
75
|
var autoFocusRequested = false
|
|
72
76
|
var stateWrapper: StateWrapper? = null
|
|
73
77
|
val layoutManager = InputLayoutManager(this)
|
|
78
|
+
private var pendingAutoFocusKeyboard = false
|
|
74
79
|
|
|
75
80
|
private var typefaceDirty = false
|
|
76
81
|
private var fontFamilyValue: String? = null
|
|
@@ -92,6 +97,17 @@ class EnrichedMarkdownTextInputView(
|
|
|
92
97
|
private var detectScrollMovement = false
|
|
93
98
|
var scrollEnabled: Boolean = true
|
|
94
99
|
|
|
100
|
+
// The markdown last handed over by JS, kept so `blockSyntax` can re-parse it. @ReactProp
|
|
101
|
+
// setters fire in undefined order, so `defaultValue` regularly lands before the flag does.
|
|
102
|
+
private var lastMarkdownFromJS: String? = null
|
|
103
|
+
|
|
104
|
+
var blockSyntax: Boolean = true
|
|
105
|
+
set(value) {
|
|
106
|
+
if (field == value) return
|
|
107
|
+
field = value
|
|
108
|
+
lastMarkdownFromJS?.let { setValueFromJS(it) }
|
|
109
|
+
}
|
|
110
|
+
|
|
95
111
|
private var mentionIndicators: LinkedHashSet<String> = linkedSetOf()
|
|
96
112
|
private var activeMentionIndicator: String? = null
|
|
97
113
|
private var activeMentionStart = -1
|
|
@@ -176,6 +192,13 @@ class EnrichedMarkdownTextInputView(
|
|
|
176
192
|
refreshAllMathChips()
|
|
177
193
|
}
|
|
178
194
|
|
|
195
|
+
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
|
|
196
|
+
super.onWindowFocusChanged(hasWindowFocus)
|
|
197
|
+
// The autofocus keyboard request may run before the window has IME focus (e.g. while a modal is
|
|
198
|
+
// still presenting), where showSoftInput() is dropped.
|
|
199
|
+
showAutoFocusKeyboardIfPending()
|
|
200
|
+
}
|
|
201
|
+
|
|
179
202
|
override fun clearFocus() {
|
|
180
203
|
super.clearFocus()
|
|
181
204
|
inputMethodManager?.hideSoftInputFromWindow(windowToken, 0)
|
|
@@ -312,6 +335,12 @@ class EnrichedMarkdownTextInputView(
|
|
|
312
335
|
if (!isComponentReady || isDuringTransaction) return
|
|
313
336
|
|
|
314
337
|
if (!isTextChanging) {
|
|
338
|
+
// Links (e.g. mentions) are atomic: snap a partial selection to the whole link, and a caret
|
|
339
|
+
// inside a link to its end. Returning lets the recursive onSelectionChanged emit for the result.
|
|
340
|
+
formattingStore.selectionAdjustedForAtomicLinks(selStart, selEnd)?.let { (newStart, newEnd) ->
|
|
341
|
+
setSelection(newStart, newEnd)
|
|
342
|
+
return
|
|
343
|
+
}
|
|
315
344
|
if (didTextChangeRecently) {
|
|
316
345
|
didTextChangeRecently = false
|
|
317
346
|
} else {
|
|
@@ -345,8 +374,18 @@ class EnrichedMarkdownTextInputView(
|
|
|
345
374
|
val rangeStart = if (preEditSelectionStart != preEditSelectionEnd) preEditSelectionStart else editStart
|
|
346
375
|
val rangeEnd = rangeStart + insertedLength
|
|
347
376
|
|
|
348
|
-
|
|
349
|
-
|
|
377
|
+
// Skip applying pending styles when the insertion is only line breaks —
|
|
378
|
+
// a phantom range over a bare newline corrupts isStyleActive() at the boundary.
|
|
379
|
+
val currentText = text
|
|
380
|
+
val insertedHasGlyphContent =
|
|
381
|
+
currentText != null &&
|
|
382
|
+
rangeEnd <= currentText.length &&
|
|
383
|
+
(rangeStart until rangeEnd).any { !currentText[it].isLineBreak() }
|
|
384
|
+
|
|
385
|
+
if (insertedHasGlyphContent) {
|
|
386
|
+
for (style in pendingStyles) {
|
|
387
|
+
formattingStore.addRange(FormattingRange(style, rangeStart, rangeEnd))
|
|
388
|
+
}
|
|
350
389
|
}
|
|
351
390
|
|
|
352
391
|
for (style in pendingStyleRemovals) {
|
|
@@ -439,6 +478,16 @@ class EnrichedMarkdownTextInputView(
|
|
|
439
478
|
}
|
|
440
479
|
}
|
|
441
480
|
|
|
481
|
+
// Copies the input's plain text to the system clipboard without disturbing
|
|
482
|
+
// the current selection. Android's system clipboard round-trip is plain text
|
|
483
|
+
// only — inline styles are not preserved for external paste targets.
|
|
484
|
+
fun copyToClipboard() {
|
|
485
|
+
val content = text
|
|
486
|
+
if (content.isNullOrEmpty()) return
|
|
487
|
+
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager ?: return
|
|
488
|
+
clipboard.setPrimaryClip(ClipData.newPlainText(null, content))
|
|
489
|
+
}
|
|
490
|
+
|
|
442
491
|
fun setLinkForSelection(url: String) {
|
|
443
492
|
val selStart = selectionStart
|
|
444
493
|
val selEnd = selectionEnd
|
|
@@ -596,7 +645,8 @@ class EnrichedMarkdownTextInputView(
|
|
|
596
645
|
}
|
|
597
646
|
|
|
598
647
|
fun setValueFromJS(markdown: String) {
|
|
599
|
-
|
|
648
|
+
lastMarkdownFromJS = markdown
|
|
649
|
+
val parsed = InputParser.parseToPlainTextAndRanges(markdown, blockSyntax)
|
|
600
650
|
blockEmitting = true
|
|
601
651
|
try {
|
|
602
652
|
runAsATransaction {
|
|
@@ -670,8 +720,16 @@ class EnrichedMarkdownTextInputView(
|
|
|
670
720
|
}
|
|
671
721
|
invalidate()
|
|
672
722
|
requestLayout()
|
|
723
|
+
// Chips only reach the text here when `defaultValue` landed before `markdownStyle`
|
|
724
|
+
// (@ReactProp order is undefined). requestLayout() cannot change a height Fabric owns —
|
|
725
|
+
// the shadow tree has to re-measure text that now carries the spans.
|
|
726
|
+
layoutManager.invalidateLayout()
|
|
673
727
|
} else if (mathStyleChanged) {
|
|
674
728
|
refreshAllMathChips()
|
|
729
|
+
// A math font size / padding change resizes every chip, so the measured height moves.
|
|
730
|
+
// Colour-only changes end up here too; store() then measures the same size and skips
|
|
731
|
+
// the state update.
|
|
732
|
+
layoutManager.invalidateLayout()
|
|
675
733
|
}
|
|
676
734
|
return changed
|
|
677
735
|
}
|
|
@@ -810,18 +868,36 @@ class EnrichedMarkdownTextInputView(
|
|
|
810
868
|
return true
|
|
811
869
|
}
|
|
812
870
|
|
|
813
|
-
|
|
871
|
+
/** Chip glyph rect in [Layout] coordinates — no padding, no scroll offset. */
|
|
872
|
+
private fun chipBoundsInLayout(
|
|
814
873
|
layout: Layout,
|
|
815
874
|
start: Int,
|
|
816
875
|
): Rect {
|
|
817
876
|
val line = layout.getLineForOffset(start)
|
|
818
|
-
val xStart =
|
|
819
|
-
val xEnd =
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
877
|
+
val xStart = layout.getPrimaryHorizontal(start).toInt()
|
|
878
|
+
val xEnd = layout.getPrimaryHorizontal(start + 1).toInt()
|
|
879
|
+
// Ordered, not raw start/end: in an RTL run xStart > xEnd, which would make an
|
|
880
|
+
// inverted Rect that contains() always rejects.
|
|
881
|
+
val left = xStart.coerceAtMost(xEnd)
|
|
882
|
+
var right = xStart.coerceAtLeast(xEnd)
|
|
883
|
+
if (right <= left) {
|
|
884
|
+
// A chip that is the last character has start + 1 == length, where some layouts
|
|
885
|
+
// report the same horizontal for both offsets. Fall back to the line's content edge
|
|
886
|
+
// so the rect keeps a real width instead of collapsing.
|
|
887
|
+
right = layout.getLineRight(line).toInt()
|
|
888
|
+
}
|
|
889
|
+
return Rect(left, layout.getLineTop(line), right, layout.getLineBottom(line))
|
|
823
890
|
}
|
|
824
891
|
|
|
892
|
+
/** Chip glyph rect in view coordinates, for reporting to JS. */
|
|
893
|
+
private fun computeChipBounds(
|
|
894
|
+
layout: Layout,
|
|
895
|
+
start: Int,
|
|
896
|
+
): Rect =
|
|
897
|
+
chipBoundsInLayout(layout, start).apply {
|
|
898
|
+
offset(totalPaddingLeft - scrollX, totalPaddingTop - scrollY)
|
|
899
|
+
}
|
|
900
|
+
|
|
825
901
|
fun mathChipPositionAtTouch(
|
|
826
902
|
x: Float,
|
|
827
903
|
y: Float,
|
|
@@ -831,13 +907,21 @@ class EnrichedMarkdownTextInputView(
|
|
|
831
907
|
val layoutNow = layout ?: return -1
|
|
832
908
|
val adjustedX = (x - totalPaddingLeft + scrollX).toInt()
|
|
833
909
|
val adjustedY = (y - totalPaddingTop + scrollY).toInt()
|
|
834
|
-
|
|
910
|
+
// getLineForVertical and getOffsetForHorizontal both clamp into the laid-out text, so
|
|
911
|
+
// a touch in the padding or in the blank space below the last line resolves onto the
|
|
912
|
+
// nearest character. Bound the touch to the text box, then require it to land inside
|
|
913
|
+
// the chip's own rect — otherwise a tap beside a chip reads as a tap on it.
|
|
914
|
+
if (adjustedY < 0 || adjustedY > layoutNow.height) return -1
|
|
835
915
|
val line = layoutNow.getLineForVertical(adjustedY)
|
|
836
916
|
val offset = layoutNow.getOffsetForHorizontal(line, adjustedX.toFloat())
|
|
837
|
-
if (offset < 0
|
|
917
|
+
if (offset < 0) return -1
|
|
838
918
|
val candidates = intArrayOf(offset, offset - 1).filter { it in 0 until editable.length }
|
|
839
919
|
for (candidate in candidates) {
|
|
840
920
|
val range = controller.chipRangeAt(editable, candidate) ?: continue
|
|
921
|
+
// An empty rect means the layout could not give us the chip's extent; accept the
|
|
922
|
+
// candidate then, rather than making the chip untappable.
|
|
923
|
+
val bounds = chipBoundsInLayout(layoutNow, range.first)
|
|
924
|
+
if (!bounds.isEmpty && !bounds.contains(adjustedX, adjustedY)) continue
|
|
841
925
|
return range.first
|
|
842
926
|
}
|
|
843
927
|
return -1
|
|
@@ -954,11 +1038,25 @@ class EnrichedMarkdownTextInputView(
|
|
|
954
1038
|
setSelection(selectionStart.coerceAtLeast(0))
|
|
955
1039
|
}
|
|
956
1040
|
|
|
1041
|
+
private fun showAutoFocusKeyboardIfPending() {
|
|
1042
|
+
if (!pendingAutoFocusKeyboard || !hasWindowFocus()) return
|
|
1043
|
+
pendingAutoFocusKeyboard = false
|
|
1044
|
+
inputMethodManager?.showSoftInput(this, 0)
|
|
1045
|
+
}
|
|
1046
|
+
|
|
957
1047
|
fun afterUpdateTransaction() {
|
|
958
1048
|
updateTypeface()
|
|
959
1049
|
if (autoFocusRequested) {
|
|
960
1050
|
autoFocusRequested = false
|
|
961
|
-
|
|
1051
|
+
pendingAutoFocusKeyboard = true
|
|
1052
|
+
post {
|
|
1053
|
+
// afterUpdateTransaction runs before onAttachedToWindow, where requestFocus()/showSoftInput()
|
|
1054
|
+
// are dropped and setTextIsSelectable(true) would reset the caret to 0. Defer to the next loop
|
|
1055
|
+
// so focus sticks and the caret lands at end (matching iOS).
|
|
1056
|
+
requestFocus()
|
|
1057
|
+
setSelection(text?.length ?: 0)
|
|
1058
|
+
showAutoFocusKeyboardIfPending()
|
|
1059
|
+
}
|
|
962
1060
|
}
|
|
963
1061
|
}
|
|
964
1062
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.input.formatting
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hides block-level markdown structure from md4c so a document parses as a single
|
|
5
|
+
* `Paragraph`. Used by `blockSyntax = false` on `EnrichedMarkdownTextInput`.
|
|
6
|
+
*
|
|
7
|
+
* Every newline is swapped for a vertical tab (`0x0B`) and one more is prepended.
|
|
8
|
+
* md4c treats VT as ordinary whitespace, never as a line break, so no line ever
|
|
9
|
+
* starts at a position where a list marker, `#`, `>` or an indented code block can
|
|
10
|
+
* be recognised — including line 1, which is what the leading sentinel is for.
|
|
11
|
+
* Inline syntax (`$...$`, `**...**`, links) still parses normally.
|
|
12
|
+
*
|
|
13
|
+
* The transform is 1:1 in characters *and* in UTF-8 bytes (VT and `\n` are both one
|
|
14
|
+
* of each), so offsets coming back out of the parser only need a flat -1 shift for
|
|
15
|
+
* the leading sentinel. iOS relies on the same property to keep its byte-to-UTF16
|
|
16
|
+
* map valid.
|
|
17
|
+
*/
|
|
18
|
+
internal object BlockSyntaxSentinel {
|
|
19
|
+
const val SENTINEL = '\u000B'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `\r\n` / `\r` collapse to `\n` (md4c normalizes line endings this way regardless,
|
|
23
|
+
* so this is pre-existing lossiness, not a new one), and any `0x0B` already present
|
|
24
|
+
* is dropped — leaving it in would make [decode] silently turn it into a newline.
|
|
25
|
+
*/
|
|
26
|
+
fun canonicalize(markdown: String): String {
|
|
27
|
+
val out = StringBuilder(markdown.length)
|
|
28
|
+
var i = 0
|
|
29
|
+
while (i < markdown.length) {
|
|
30
|
+
when (markdown[i]) {
|
|
31
|
+
'\r' -> {
|
|
32
|
+
out.append('\n')
|
|
33
|
+
if (i + 1 < markdown.length && markdown[i + 1] == '\n') i++
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
SENTINEL -> {}
|
|
37
|
+
|
|
38
|
+
else -> {
|
|
39
|
+
out.append(markdown[i])
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
i++
|
|
43
|
+
}
|
|
44
|
+
return out.toString()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Prepends one sentinel and replaces every `\n` with one. Length grows by exactly 1. */
|
|
48
|
+
fun encode(canonical: String): String {
|
|
49
|
+
val out = StringBuilder(canonical.length + 1)
|
|
50
|
+
out.append(SENTINEL)
|
|
51
|
+
for (c in canonical) out.append(if (c == '\n') SENTINEL else c)
|
|
52
|
+
return out.toString()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Reverses [encode] on the parser's plain text. Returns `null` when the leading
|
|
57
|
+
* sentinel is missing — the caller then falls back to the canonical source with no
|
|
58
|
+
* ranges rather than emitting text shifted by an offset that no longer holds.
|
|
59
|
+
*/
|
|
60
|
+
fun decode(plainText: String): String? {
|
|
61
|
+
if (plainText.isEmpty() || plainText[0] != SENTINEL) return null
|
|
62
|
+
val out = StringBuilder(plainText.length - 1)
|
|
63
|
+
for (i in 1 until plainText.length) {
|
|
64
|
+
val c = plainText[i]
|
|
65
|
+
out.append(if (c == SENTINEL) '\n' else c)
|
|
66
|
+
}
|
|
67
|
+
return out.toString()
|
|
68
|
+
}
|
|
69
|
+
}
|
package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/FormattingStore.kt
CHANGED
|
@@ -34,6 +34,25 @@ class FormattingStore {
|
|
|
34
34
|
end: Int,
|
|
35
35
|
): Boolean = ranges.any { it.type == type && it.start < end && it.end > start }
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Snaps a selection so it never partially overlaps an atomic link: a partial selection expands to
|
|
39
|
+
* the whole link, a caret inside a link moves to its end. Returns the adjusted (start, end) or null.
|
|
40
|
+
*/
|
|
41
|
+
fun selectionAdjustedForAtomicLinks(
|
|
42
|
+
start: Int,
|
|
43
|
+
end: Int,
|
|
44
|
+
): Pair<Int, Int>? {
|
|
45
|
+
if (start != end) {
|
|
46
|
+
var newStart = start
|
|
47
|
+
var newEnd = end
|
|
48
|
+
rangeOfType(StyleType.LINK, newStart)?.let { newStart = minOf(newStart, it.start) }
|
|
49
|
+
if (newEnd > 0) rangeOfType(StyleType.LINK, newEnd - 1)?.let { newEnd = maxOf(newEnd, it.end) }
|
|
50
|
+
return if (newStart != start || newEnd != end) Pair(newStart, newEnd) else null
|
|
51
|
+
}
|
|
52
|
+
val link = rangeOfType(StyleType.LINK, start)
|
|
53
|
+
return if (link != null && start > link.start && start < link.end) Pair(link.end, link.end) else null
|
|
54
|
+
}
|
|
55
|
+
|
|
37
56
|
fun addRange(newRange: FormattingRange) {
|
|
38
57
|
var mergedStart = newRange.start
|
|
39
58
|
var mergedEnd = newRange.end
|
package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputParser.kt
CHANGED
|
@@ -7,6 +7,7 @@ import com.swmansion.enriched.markdown.parser.MarkdownASTNode
|
|
|
7
7
|
import com.swmansion.enriched.markdown.parser.MarkdownASTNode.NodeType
|
|
8
8
|
import com.swmansion.enriched.markdown.parser.Md4cFlags
|
|
9
9
|
import com.swmansion.enriched.markdown.parser.Parser
|
|
10
|
+
import com.swmansion.enriched.markdown.parser.isTopLevelBlock
|
|
10
11
|
import com.swmansion.enriched.markdown.utils.common.FeatureFlags
|
|
11
12
|
|
|
12
13
|
data class ParseResult(
|
|
@@ -18,25 +19,96 @@ data class ParseResult(
|
|
|
18
19
|
object InputParser {
|
|
19
20
|
private const val ATTACHMENT_CHAR = ''
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
// A source paragraph break: a newline followed by one or more blank lines.
|
|
23
|
+
private val PARAGRAPH_BREAK_RE = Regex("\\n[ \\t]*(?:\\n[ \\t]*)+")
|
|
24
|
+
|
|
25
|
+
fun parseToPlainTextAndRanges(
|
|
26
|
+
markdown: String,
|
|
27
|
+
blockSyntax: Boolean = true,
|
|
28
|
+
): ParseResult {
|
|
22
29
|
if (markdown.isEmpty()) {
|
|
23
30
|
return ParseResult("", emptyList())
|
|
24
31
|
}
|
|
25
32
|
|
|
26
|
-
|
|
33
|
+
if (!blockSyntax) {
|
|
34
|
+
return parseWithoutBlockSyntax(markdown)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return parseCompleted(InputRemend.complete(markdown), markdown, blockSeparators = true)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parses with every block construct hidden behind [BlockSyntaxSentinel], so the whole
|
|
42
|
+
* document lands in one `Paragraph` and list markers, `#`, `>` and friends stay in the
|
|
43
|
+
* text verbatim. `InputRemend` is skipped as well: its speculative closing delimiters
|
|
44
|
+
* are the other source of characters the serializer cannot replay.
|
|
45
|
+
*/
|
|
46
|
+
private fun parseWithoutBlockSyntax(markdown: String): ParseResult {
|
|
47
|
+
val canonical = BlockSyntaxSentinel.canonicalize(markdown)
|
|
48
|
+
if (canonical.isEmpty()) {
|
|
49
|
+
return ParseResult("", emptyList())
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// No block separators: the encoded text has no newline for md4c to drop, so every
|
|
53
|
+
// newline the replay would add is spurious. `$$...$$` is the case that proves it —
|
|
54
|
+
// md4c reports display math as a top-level block even mid-line under the sentinel.
|
|
55
|
+
val parsed = parseCompleted(BlockSyntaxSentinel.encode(canonical), canonical, blockSeparators = false)
|
|
56
|
+
val decoded =
|
|
57
|
+
BlockSyntaxSentinel.decode(parsed.plainText)
|
|
58
|
+
?: return ParseResult(canonical, emptyList())
|
|
59
|
+
|
|
60
|
+
// Only the leading sentinel is removed, so every offset shifts by exactly one.
|
|
61
|
+
val ranges =
|
|
62
|
+
parsed.formattingRanges.mapNotNull { range ->
|
|
63
|
+
val start = range.start - 1
|
|
64
|
+
val end = range.end - 1
|
|
65
|
+
if (start < 0 || end > decoded.length || end <= start) {
|
|
66
|
+
null
|
|
67
|
+
} else {
|
|
68
|
+
FormattingRange(range.type, start, end, range.url)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
val atoms =
|
|
72
|
+
parsed.mathAtoms.mapNotNull { atom ->
|
|
73
|
+
val position = atom.position - 1
|
|
74
|
+
if (position < 0 || position >= decoded.length) null else atom.copy(position = position)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return ParseResult(decoded, ranges, atoms)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private fun parseCompleted(
|
|
81
|
+
completed: String,
|
|
82
|
+
fallback: String,
|
|
83
|
+
blockSeparators: Boolean,
|
|
84
|
+
): ParseResult {
|
|
27
85
|
val flags =
|
|
28
86
|
Md4cFlags(
|
|
29
87
|
underline = true,
|
|
30
88
|
latexMath = FeatureFlags.IS_MATH_ENABLED,
|
|
31
89
|
permissiveAutolinks = false,
|
|
32
90
|
)
|
|
33
|
-
val ast = Parser.shared.parseMarkdown(completed, flags) ?: return ParseResult(
|
|
91
|
+
val ast = Parser.shared.parseMarkdown(completed, flags) ?: return ParseResult(fallback, emptyList())
|
|
34
92
|
|
|
35
93
|
val plainText = StringBuilder()
|
|
36
94
|
val ranges = mutableListOf<FormattingRange>()
|
|
37
95
|
val mathAtoms = mutableListOf<MathAtom>()
|
|
38
96
|
|
|
39
|
-
|
|
97
|
+
// md4c collapses any blank-line run into a single break, unlike iOS which keeps them.
|
|
98
|
+
// Re-read the real runs so each break replays its original number of newlines.
|
|
99
|
+
val blankRuns =
|
|
100
|
+
if (blockSeparators) {
|
|
101
|
+
ArrayDeque(
|
|
102
|
+
PARAGRAPH_BREAK_RE
|
|
103
|
+
.findAll(completed.trim())
|
|
104
|
+
.map { match -> match.value.count { it == '\n' } }
|
|
105
|
+
.toList(),
|
|
106
|
+
)
|
|
107
|
+
} else {
|
|
108
|
+
ArrayDeque()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
walkNode(ast, plainText, ranges, mathAtoms, ArrayDeque(), blankRuns, blockSeparators)
|
|
40
112
|
|
|
41
113
|
return ParseResult(plainText.toString(), ranges, mathAtoms)
|
|
42
114
|
}
|
|
@@ -47,6 +119,8 @@ object InputParser {
|
|
|
47
119
|
ranges: MutableList<FormattingRange>,
|
|
48
120
|
mathAtoms: MutableList<MathAtom>,
|
|
49
121
|
activeStyles: ArrayDeque<ActiveStyle>,
|
|
122
|
+
blankRuns: ArrayDeque<Int>,
|
|
123
|
+
blockSeparators: Boolean,
|
|
50
124
|
) {
|
|
51
125
|
if (node.type == NodeType.LatexMathInline || node.type == NodeType.LatexMathDisplay) {
|
|
52
126
|
val latex = extractLatex(node)
|
|
@@ -75,8 +149,12 @@ object InputParser {
|
|
|
75
149
|
plainText.append("\n")
|
|
76
150
|
}
|
|
77
151
|
|
|
78
|
-
for (child in node.children) {
|
|
79
|
-
|
|
152
|
+
for ((index, child) in node.children.withIndex()) {
|
|
153
|
+
// Keep the source's blank lines between top-level blocks (md4c drops them, iOS keeps them).
|
|
154
|
+
if (blockSeparators && index > 0 && child.type.isTopLevelBlock() && plainText.isNotEmpty()) {
|
|
155
|
+
plainText.append("\n".repeat(blankRuns.removeFirstOrNull() ?: 2))
|
|
156
|
+
}
|
|
157
|
+
walkNode(child, plainText, ranges, mathAtoms, activeStyles, blankRuns, blockSeparators)
|
|
80
158
|
}
|
|
81
159
|
|
|
82
160
|
if (styleType != null) {
|
package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputLayoutManager.kt
CHANGED
|
@@ -8,21 +8,49 @@ class InputLayoutManager(
|
|
|
8
8
|
) {
|
|
9
9
|
private var forceHeightRecalculationCounter = 0
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* An invalidation that arrived before `stateWrapper` existed. Fabric applies every prop
|
|
13
|
+
* (and runs `onAfterUpdateTransaction`) inside `ViewManager.updateProperties`, which runs
|
|
14
|
+
* *before* `updateState` assigns the wrapper — so the whole mount transaction invalidates
|
|
15
|
+
* into the void. Without this flag the measurement store stays empty and Yoga keeps the
|
|
16
|
+
* `initialMeasure()` approximation until the next text/prop change.
|
|
17
|
+
*/
|
|
18
|
+
private var pending = false
|
|
19
|
+
|
|
11
20
|
fun invalidateLayout() {
|
|
12
|
-
|
|
21
|
+
val stateWrapper = view.stateWrapper
|
|
22
|
+
if (stateWrapper == null) {
|
|
23
|
+
pending = true
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
pending = false
|
|
13
27
|
|
|
14
28
|
val text = view.text
|
|
15
29
|
val paint = view.paint
|
|
16
30
|
|
|
31
|
+
// First call for this view measures against cachedWidth = 0 and stores a throwaway size;
|
|
32
|
+
// Yoga immediately re-measures through getMeasureById() with the real width and overwrites
|
|
33
|
+
// it. The state update below is what makes that second pass happen.
|
|
17
34
|
val needUpdate = InputMeasurementStore.store(view.id, text, paint)
|
|
18
35
|
if (!needUpdate) return
|
|
19
36
|
|
|
20
37
|
val state = Arguments.createMap()
|
|
21
38
|
state.putInt("forceHeightRecalculationCounter", forceHeightRecalculationCounter++)
|
|
22
|
-
|
|
39
|
+
stateWrapper.updateState(state)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Replays an invalidation that was dropped while `stateWrapper` was null. Called once the
|
|
44
|
+
* wrapper is assigned, i.e. after every prop of the mount transaction has been applied — so
|
|
45
|
+
* the text carries its math chips and the paint carries the resolved typeface/size.
|
|
46
|
+
*/
|
|
47
|
+
fun flushPendingInvalidation() {
|
|
48
|
+
if (!pending) return
|
|
49
|
+
invalidateLayout()
|
|
23
50
|
}
|
|
24
51
|
|
|
25
52
|
fun release() {
|
|
53
|
+
pending = false
|
|
26
54
|
InputMeasurementStore.release(view.id)
|
|
27
55
|
}
|
|
28
56
|
}
|
package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputMeasurementStore.kt
CHANGED
|
@@ -9,6 +9,9 @@ import com.facebook.react.bridge.ReadableMap
|
|
|
9
9
|
import com.facebook.react.uimanager.PixelUtil
|
|
10
10
|
import com.facebook.yoga.YogaMeasureMode
|
|
11
11
|
import com.facebook.yoga.YogaMeasureOutput
|
|
12
|
+
import com.swmansion.enriched.markdown.input.formatting.InputParser
|
|
13
|
+
import com.swmansion.enriched.markdown.utils.common.getBooleanOrDefault
|
|
14
|
+
import com.swmansion.enriched.markdown.utils.common.getFloatOrDefault
|
|
12
15
|
import java.util.concurrent.ConcurrentHashMap
|
|
13
16
|
|
|
14
17
|
object InputMeasurementStore {
|
|
@@ -93,8 +96,16 @@ object InputMeasurementStore {
|
|
|
93
96
|
width: Float,
|
|
94
97
|
props: ReadableMap?,
|
|
95
98
|
): Long {
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
// Measure the rendered plain text, not the raw markdown. A mention link such as
|
|
100
|
+
// [Label](placeholder://x) hides its URL in the source, so measuring the markdown counts those
|
|
101
|
+
// invisible characters and over-estimates the height into extra lines, until the next text
|
|
102
|
+
// change forces a re-measure. Parsing first matches what the editor actually renders.
|
|
103
|
+
val markdown = props?.getString("defaultValue") ?: props?.getString("placeholder") ?: "I"
|
|
104
|
+
// Must read the same flag the view parses with, or the measured height and the
|
|
105
|
+
// rendered height disagree: `blockSyntax = false` keeps list markers and `#` as text.
|
|
106
|
+
val blockSyntax = props.getBooleanOrDefault("blockSyntax", true)
|
|
107
|
+
val text = InputParser.parseToPlainTextAndRanges(markdown, blockSyntax).plainText
|
|
108
|
+
val fontSize = props.getFloatOrDefault("fontSize", 16f)
|
|
98
109
|
val spSize = kotlin.math.ceil(PixelUtil.toPixelFromSP(fontSize).toDouble()).toFloat()
|
|
99
110
|
|
|
100
111
|
val defaultView = AppCompatEditText(context)
|
|
@@ -13,6 +13,7 @@ open class MathChipStyle(
|
|
|
13
13
|
val paddingVertical: Float,
|
|
14
14
|
val selectedBackgroundColor: Int?,
|
|
15
15
|
val selectedBorderColor: Int?,
|
|
16
|
+
val fontWeight: String = "normal",
|
|
16
17
|
) {
|
|
17
18
|
open fun isBlock(): Boolean = false
|
|
18
19
|
}
|
|
@@ -28,6 +29,7 @@ class MathBlockStyle(
|
|
|
28
29
|
paddingVertical: Float,
|
|
29
30
|
selectedBackgroundColor: Int?,
|
|
30
31
|
selectedBorderColor: Int?,
|
|
32
|
+
fontWeight: String = "normal",
|
|
31
33
|
val marginTop: Float,
|
|
32
34
|
val marginBottom: Float,
|
|
33
35
|
val alignment: Layout.Alignment,
|
|
@@ -42,6 +44,7 @@ class MathBlockStyle(
|
|
|
42
44
|
paddingVertical,
|
|
43
45
|
selectedBackgroundColor,
|
|
44
46
|
selectedBorderColor,
|
|
47
|
+
fontWeight,
|
|
45
48
|
) {
|
|
46
49
|
override fun isBlock(): Boolean = true
|
|
47
50
|
}
|