react-native-enriched-markdown-scaffold 0.7.4-scaffold.9 → 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/README.md +54 -8
- package/ReactNativeEnrichedMarkdown.podspec +82 -73
- package/android/build.gradle +188 -1
- package/android/consumer-rules.pro +1 -1
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +27 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +7 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerDelegate.java +24 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerInterface.java +8 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +27 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +7 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +223 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +183 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +65 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +1086 -61
- package/android/src/main/baseline-prof.txt +1 -1
- package/android/src/main/cpp/jni-adapter.cpp +74 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +351 -13
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +62 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +85 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +160 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextLayoutManager.kt +10 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +76 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +126 -10
- package/android/src/main/java/com/swmansion/enriched/markdown/events/CopyPressEvent.kt +24 -0
- 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/EnrichedMarkdownTextInputManager.kt +54 -4
- package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +923 -462
- package/android/src/main/java/com/swmansion/enriched/markdown/input/MarkdownClipboard.kt +42 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/BlockEditCoordinator.kt +196 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/ClipboardCoordinator.kt +73 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/EditContext.kt +19 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/EditPipeline.kt +248 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/EditSession.kt +52 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/InputConnectionWrapper.kt +108 -7
- package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/LinkCoordinator.kt +69 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/MarkdownTextWatcher.kt +3 -3
- package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/MentionCoordinator.kt +118 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/events/OnChangeStateEvent.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/events/OnContextMenuItemPressEvent.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/events/OnInputKeyPressEvent.kt +24 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/events/OnPasteEvent.kt +24 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/BlockStore.kt +254 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/FormattingStore.kt +80 -87
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputFormatter.kt +109 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputParser.kt +116 -22
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/MarkdownSerializer.kt +167 -38
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/RangeEditAdjustment.kt +149 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputEventEmitter.kt +66 -5
- package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputMeasurementStore.kt +4 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/input/model/BlockRange.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/model/BlockType.kt +48 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/model/FormattingRange.kt +9 -4
- package/android/src/main/java/com/swmansion/enriched/markdown/input/model/InputFormatterStyle.kt +39 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/input/model/MathAtom.kt +20 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/model/MathChipStyle.kt +1 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/input/model/MutableRangeBounds.kt +11 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/spans/InputBulletSpan.kt +82 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/spans/InputHeadingSpan.kt +58 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/spans/InputListItemSpacingSpan.kt +37 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/spans/InputOrderedListMarkerSpan.kt +63 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/styles/BlockHandler.kt +54 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/styles/HeadingBlockHandler.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/styles/OrderedListBlockHandler.kt +38 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/styles/UnorderedListBlockHandler.kt +37 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/toolbar/InputContextMenu.kt +8 -26
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockStyleContext.kt +9 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +100 -12
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +16 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +4 -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/ListContextManager.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt +74 -18
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +43 -4
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +9 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SoftBreakRenderer.kt +16 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +35 -28
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/BaseListSpan.kt +24 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/BlockquoteSpan.kt +174 -24
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +20 -82
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBlockSpan.kt +13 -16
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeSpan.kt +1 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/EmphasisSpan.kt +1 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +182 -46
- 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/spans/ListItemSpacingSpan.kt +33 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/ListMarkerAnchorSpan.kt +38 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/OrderedListSpan.kt +2 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrongSpan.kt +1 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/TaskListSpan.kt +3 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/UnorderedListSpan.kt +3 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +6 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +7 -1
- 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/ListStyle.kt +4 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/MathStyle.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt +13 -4
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/TableStyle.kt +6 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/CodeBlockHighlighter.kt +90 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/CodeBlockNode.kt +57 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/FeatureFlags.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownSegment.kt +9 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +84 -7
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/ReadableMapExtensions.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/RenderedSegment.kt +10 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/SegmentSignature.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/StreamingMarkdownFilter.kt +83 -5
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/serialization/MarkdownASTSerializer.kt +4 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/MarkdownStyleParser.kt +74 -44
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/PasteTextUtils.kt +52 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/ImageCache.kt +26 -3
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/ImageDownloader.kt +22 -9
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/LocalImageLoader.kt +161 -0
- 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/extensions/TextPaintExtensions.kt +23 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/interaction/CheckboxTouchHelper.kt +7 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/interaction/TaskListTapUtils.kt +41 -21
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/span/BlockSpacing.kt +15 -8
- 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/LinkLongPressMovementMethod.kt +58 -27
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +72 -16
- 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 +414 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/views/ContextMenuPopup.kt +9 -6
- package/android/src/main/java/com/swmansion/enriched/markdown/views/CopyGlyph.kt +101 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +61 -11
- package/android/src/main/jni/CMakeLists.txt +53 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.cpp +10 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.h +11 -2
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp +10 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h +12 -2
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.cpp +9 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.h +24 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h +16 -0
- package/android/src/math/java/com/swmansion/enriched/markdown/input/math/MathChipControllerImpl.kt +21 -4
- package/android/src/math/java/com/swmansion/enriched/markdown/renderer/MathInlineRenderer.kt +8 -2
- package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathChipSpan.kt +1 -1
- 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 +26 -8
- package/cpp/highlight/CodeBlockHighlighter.cpp +284 -0
- package/cpp/highlight/CodeBlockHighlighter.hpp +66 -0
- package/cpp/highlight/CodeBlockLanguages.cpp +138 -0
- package/cpp/highlight/CodeBlockLanguages.hpp +25 -0
- package/cpp/highlight/HighlightGrammars.hpp +28 -0
- package/cpp/highlight/code_highlight_podspec.rb +211 -0
- package/cpp/highlight/gen-registry.mjs +155 -0
- package/cpp/highlight/grammar-versions.json +159 -0
- package/cpp/highlight/vendor/generated/generated_queries.h +1665 -0
- package/cpp/highlight/vendor/generated/generated_registry.cpp +59 -0
- package/cpp/parser/MD4CParser.cpp +173 -61
- package/cpp/parser/MD4CParser.hpp +1 -0
- package/cpp/parser/MarkdownASTNode.hpp +2 -1
- package/cpp/wasm/ASTSerializer.cpp +2 -0
- package/cpp/wasm/build.sh +1 -0
- package/cpp/wasm/md4c_wasm.cpp +3 -1
- package/docs/ACCESSIBILITY.md +14 -2
- package/docs/API_REFERENCE.md +405 -12
- package/docs/BREAKING_CHANGES.md +129 -0
- package/docs/CODE_HIGHLIGHT.md +133 -0
- package/docs/COPY_OPTIONS.md +61 -3
- package/docs/ELEMENTS_STRUCTURE.md +52 -4
- package/docs/IMAGE_CACHING.md +30 -0
- package/docs/INPUT.md +110 -11
- package/docs/LATEX_MATH.md +87 -45
- package/docs/MACOS.md +1 -1
- package/docs/MARKDOWN_STREAMING.md +27 -1
- package/docs/MENTIONS.md +8 -1
- package/docs/NATIVE_ASSETS.md +127 -0
- package/docs/RTL.md +1 -1
- package/docs/STYLES.md +73 -6
- package/docs/TESTING.md +112 -0
- package/docs/TEXT.md +18 -6
- package/docs/WEB.md +4 -1
- package/ios/EnrichedMarkdown.h +7 -0
- package/ios/EnrichedMarkdown.mm +583 -76
- package/ios/EnrichedMarkdownText.h +7 -0
- package/ios/EnrichedMarkdownText.mm +299 -73
- package/ios/attachments/ENRMImageAttachment.h +9 -0
- package/ios/attachments/ENRMImageAttachment.m +143 -40
- package/ios/attachments/ENRMMathInlineAttachment+macOS.m +2 -1
- package/ios/attachments/ENRMMathInlineAttachment.h +5 -0
- package/ios/attachments/ENRMMathInlineAttachment.m +50 -82
- package/ios/attachments/ENRMMathInlineAttachmentShared.h +2 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +223 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +183 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +65 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +1086 -61
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +181 -2
- package/ios/input/ENRMAutoLinkDetector.h +2 -1
- package/ios/input/ENRMAutoLinkDetector.mm +79 -41
- package/ios/input/ENRMBlockEditCoordinator.h +71 -0
- package/ios/input/ENRMBlockEditCoordinator.mm +195 -0
- package/ios/input/ENRMBlockRange.h +28 -0
- package/ios/input/ENRMBlockRange.mm +30 -0
- package/ios/input/ENRMBlockStore.h +56 -0
- package/ios/input/ENRMBlockStore.mm +275 -0
- package/ios/input/ENRMClipboardCoordinator.h +45 -0
- package/ios/input/ENRMClipboardCoordinator.mm +120 -0
- package/ios/input/ENRMDetectorPipeline.h +2 -1
- package/ios/input/ENRMEditPipeline.h +76 -0
- package/ios/input/ENRMEditPipeline.mm +228 -0
- package/ios/input/ENRMEditSession.h +36 -0
- package/ios/input/ENRMEditSession.mm +63 -0
- package/ios/input/ENRMFormattingStore.h +17 -0
- package/ios/input/ENRMFormattingStore.mm +95 -105
- package/ios/input/ENRMInputBlockType.h +91 -0
- package/ios/input/ENRMInputBlockType.mm +5 -0
- package/ios/input/ENRMInputDecorationDrawer.h +18 -0
- package/ios/input/ENRMInputEventEmitter.h +56 -0
- package/ios/input/ENRMInputEventEmitter.mm +308 -0
- package/ios/input/ENRMInputFormatter.h +29 -69
- package/ios/input/ENRMInputFormatter.mm +216 -124
- package/ios/input/ENRMInputFormatterStyle.h +99 -0
- package/ios/input/ENRMInputFormatterStyle.mm +188 -0
- package/ios/input/ENRMInputLayoutManager.h +9 -0
- package/ios/input/ENRMInputLayoutManager.mm +30 -0
- package/ios/input/ENRMInputListMarkerDrawer.h +32 -0
- package/ios/input/ENRMInputListMarkerDrawer.mm +426 -0
- package/ios/input/ENRMInputParser.h +5 -6
- package/ios/input/ENRMInputParser.mm +461 -178
- package/ios/input/ENRMInputRemend.mm +2 -2
- package/ios/input/ENRMInputStyleStateBuilder.h +40 -0
- package/ios/input/ENRMInputStyleStateBuilder.mm +60 -0
- package/ios/input/ENRMInputTextView.mm +103 -8
- package/ios/input/ENRMInputTypingAttributesController.h +42 -0
- package/ios/input/ENRMInputTypingAttributesController.mm +203 -0
- package/ios/input/ENRMLinkCoordinator.h +41 -0
- package/ios/input/ENRMLinkCoordinator.mm +89 -0
- package/ios/input/ENRMMarkdownSerializer.h +29 -5
- package/ios/input/ENRMMarkdownSerializer.mm +170 -48
- package/ios/input/ENRMMathAtom.h +26 -0
- package/ios/input/ENRMMathAtom.mm +12 -0
- package/ios/input/ENRMMentionCoordinator.h +45 -0
- package/ios/input/ENRMMentionCoordinator.mm +167 -0
- package/ios/input/ENRMRangeEditAdjustment.h +38 -0
- package/ios/input/ENRMRangeEditAdjustment.mm +91 -0
- package/ios/input/ENRMRangeStoreUtils.h +39 -0
- package/ios/input/EnrichedMarkdownTextInput+ContextMenu.mm +6 -6
- package/ios/input/EnrichedMarkdownTextInput+Internal.h +11 -3
- package/ios/input/EnrichedMarkdownTextInput.h +7 -1
- package/ios/input/EnrichedMarkdownTextInput.mm +1089 -1023
- package/ios/input/InputStylePropsUtils.h +109 -68
- package/ios/input/internals/EnrichedMarkdownTextInputShadowNode.mm +2 -1
- package/ios/input/styles/ENRMBlockHandler.h +46 -0
- package/ios/input/styles/ENRMHeadingBlockHandler.h +15 -0
- package/ios/input/styles/ENRMHeadingBlockHandler.mm +38 -0
- package/ios/input/styles/ENRMOrderedListBlockHandler.h +14 -0
- package/ios/input/styles/ENRMOrderedListBlockHandler.mm +48 -0
- package/ios/input/styles/ENRMUnorderedListBlockHandler.h +15 -0
- package/ios/input/styles/ENRMUnorderedListBlockHandler.mm +51 -0
- package/ios/internals/ENRMViewFreeMeasurement.h +331 -0
- package/ios/internals/EnrichedMarkdownShadowNode.h +1 -3
- package/ios/internals/EnrichedMarkdownShadowNode.mm +16 -20
- package/ios/internals/EnrichedMarkdownTextShadowNode.h +1 -3
- package/ios/internals/EnrichedMarkdownTextShadowNode.mm +15 -20
- package/ios/internals/MeasurementCache.h +41 -9
- package/ios/internals/ShadowMeasurementUtils.h +63 -58
- package/ios/math/ENRMMathChipController.h +74 -0
- package/ios/math/ENRMMathChipController.mm +476 -0
- package/ios/math/ENRMRaTeXBridge.swift +27 -6
- package/ios/parser/ENRMMarkdownParser.h +1 -8
- package/ios/parser/ENRMMarkdownParser.mm +2 -11
- package/ios/parser/MarkdownASTNode.h +2 -1
- package/ios/parser/MarkdownParserBridge.mm +4 -0
- package/ios/renderer/AttributedRenderer.m +20 -2
- package/ios/renderer/BaseRenderer.h +17 -2
- package/ios/renderer/BaseRenderer.m +30 -0
- package/ios/renderer/BlockquoteRenderer.h +1 -1
- package/ios/renderer/BlockquoteRenderer.m +144 -35
- package/ios/renderer/CodeBlockRenderer.h +1 -1
- package/ios/renderer/CodeBlockRenderer.m +27 -23
- package/ios/renderer/CodeRenderer.h +1 -1
- package/ios/renderer/CodeRenderer.m +3 -1
- package/ios/renderer/ENRMImageRenderer.h +1 -1
- package/ios/renderer/ENRMImageRenderer.m +5 -1
- package/ios/renderer/ENRMMathInlineRenderer.h +1 -1
- package/ios/renderer/ENRMMathInlineRenderer.m +6 -1
- package/ios/renderer/ENRMSpoilerRenderer.h +1 -1
- package/ios/renderer/ENRMSpoilerRenderer.m +3 -1
- package/ios/renderer/EmphasisRenderer.h +1 -1
- package/ios/renderer/EmphasisRenderer.m +3 -1
- package/ios/renderer/HeadingRenderer.h +1 -1
- package/ios/renderer/HeadingRenderer.m +13 -6
- package/ios/renderer/HighlightRenderer.h +1 -1
- package/ios/renderer/HighlightRenderer.m +3 -1
- package/ios/renderer/LinkRenderer.h +1 -1
- package/ios/renderer/LinkRenderer.m +50 -16
- package/ios/renderer/ListItemRenderer.h +13 -1
- package/ios/renderer/ListItemRenderer.m +102 -44
- package/ios/renderer/ListRenderer.h +2 -2
- package/ios/renderer/ListRenderer.m +63 -25
- package/ios/renderer/ParagraphRenderer.h +1 -1
- package/ios/renderer/ParagraphRenderer.m +16 -8
- package/ios/renderer/RenderContext.h +10 -0
- package/ios/renderer/RenderContext.m +44 -0
- package/ios/renderer/RendererFactory.m +14 -0
- package/ios/renderer/StrikethroughRenderer.h +1 -1
- package/ios/renderer/StrikethroughRenderer.m +3 -1
- package/ios/renderer/StrongRenderer.h +1 -1
- package/ios/renderer/StrongRenderer.m +3 -1
- package/ios/renderer/SubscriptRenderer.h +1 -1
- package/ios/renderer/SubscriptRenderer.m +3 -1
- package/ios/renderer/SuperscriptRenderer.h +1 -1
- package/ios/renderer/SuperscriptRenderer.m +3 -1
- package/ios/renderer/TextRenderer.h +2 -2
- package/ios/renderer/TextRenderer.m +3 -1
- package/ios/renderer/ThematicBreakRenderer.h +1 -1
- package/ios/renderer/ThematicBreakRenderer.m +3 -1
- package/ios/renderer/UnderlineRenderer.h +1 -1
- package/ios/renderer/UnderlineRenderer.m +3 -1
- package/ios/styles/StyleConfig.h +42 -0
- package/ios/styles/StyleConfig.mm +216 -0
- package/ios/utils/BlockquoteBorder.h +6 -0
- package/ios/utils/BlockquoteBorder.m +249 -52
- package/ios/utils/CodeBackground.m +3 -65
- package/ios/utils/CodeBlockBackground.m +16 -2
- package/ios/utils/ENRMAtomicSize.h +65 -0
- package/ios/utils/ENRMBlockMenu.h +66 -0
- package/ios/utils/ENRMChipDrawing.h +24 -0
- package/ios/utils/ENRMChipDrawing.m +83 -0
- package/ios/utils/ENRMCodeBlockContent.h +37 -0
- package/ios/utils/ENRMCodeBlockContent.mm +79 -0
- package/ios/utils/ENRMCodeBlockHighlighter.h +37 -0
- package/ios/utils/ENRMCodeBlockHighlighter.mm +51 -0
- package/ios/utils/ENRMFeatureFlags.h +7 -0
- package/ios/utils/ENRMImageDownloader.h +11 -1
- package/ios/utils/ENRMImageDownloader.m +60 -16
- 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/ENRMLocalImageLoader.h +31 -0
- package/ios/utils/ENRMLocalImageLoader.m +68 -0
- package/ios/utils/ENRMMathFallback.h +26 -0
- package/ios/utils/ENRMMathFallback.m +18 -0
- package/ios/utils/ENRMSelectionChange.h +120 -0
- package/ios/utils/ENRMSelectionChange.m +282 -0
- package/ios/utils/ENRMTextViewSetup.h +35 -17
- package/ios/utils/EditMenuUtils+macOS.m +4 -1
- package/ios/utils/EditMenuUtils.h +35 -6
- package/ios/utils/EditMenuUtils.m +188 -11
- package/ios/utils/HTMLGenerator.m +2 -1
- package/ios/utils/ImageRequestHeaderUtils.h +35 -0
- package/ios/utils/LastElementUtils.h +1 -0
- package/ios/utils/ListMarkerDrawer.m +29 -42
- package/ios/utils/MarkdownASTSerializer.m +4 -0
- package/ios/utils/MarkdownAccessibilityElementBuilder.m +3 -1
- package/ios/utils/MarkdownExtractor.m +2 -1
- package/ios/utils/ParagraphStyleUtils.h +5 -0
- package/ios/utils/ParagraphStyleUtils.m +77 -17
- package/ios/utils/RTFExportUtils.m +2 -0
- package/ios/utils/RenderedMarkdownSegment.h +13 -1
- package/ios/utils/RenderedMarkdownSegment.m +20 -0
- package/ios/utils/SegmentRenderer.m +17 -4
- package/ios/utils/StreamingMarkdownFilter.h +11 -1
- package/ios/utils/StreamingMarkdownFilter.m +98 -2
- package/ios/utils/StylePropsUtils.h +110 -1
- package/ios/utils/TaskListTapUtils.m +24 -0
- package/ios/utils/TextViewLayoutManager.mm +3 -0
- package/ios/views/ENRMCodeBlockContainerView.h +60 -0
- package/ios/views/ENRMCodeBlockContainerView.m +569 -0
- package/ios/views/ENRMMathContainerView.h +23 -0
- package/ios/views/ENRMMathContainerView.m +87 -20
- package/ios/views/TableContainerView.h +32 -0
- package/ios/views/TableContainerView.m +291 -175
- package/jest.d.ts +1 -0
- package/jest.js +5 -0
- package/lib/module/EnrichedMarkdownNativeComponent.ts +203 -1
- package/lib/module/EnrichedMarkdownTextInput.js +84 -5
- package/lib/module/EnrichedMarkdownTextInput.js.map +1 -1
- package/lib/module/EnrichedMarkdownTextInputNativeComponent.ts +78 -5
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +204 -1
- package/lib/module/headingDefaults.js +34 -0
- package/lib/module/headingDefaults.js.map +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/jest/index.js +205 -0
- package/lib/module/jest/index.js.map +1 -0
- package/lib/module/native/EnrichedMarkdownText.js +199 -15
- package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
- package/lib/module/normalizeMarkdownStyle.js +98 -23
- package/lib/module/normalizeMarkdownStyle.js.map +1 -1
- package/lib/module/normalizeMarkdownStyle.web.js +73 -22
- package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
- package/lib/module/normalizeMarkdownTextInputStyle.js +62 -15
- package/lib/module/normalizeMarkdownTextInputStyle.js.map +1 -1
- package/lib/module/normalizeMenuItem.js +1 -1
- package/lib/module/utils/textInputState.js +26 -0
- package/lib/module/utils/textInputState.js.map +1 -0
- package/lib/module/web/EnrichedMarkdownText.js +15 -8
- package/lib/module/web/EnrichedMarkdownText.js.map +1 -1
- package/lib/module/web/parseMarkdown.js +4 -3
- package/lib/module/web/parseMarkdown.js.map +1 -1
- package/lib/module/web/renderers/InlineRenderers.js +102 -3
- package/lib/module/web/renderers/InlineRenderers.js.map +1 -1
- package/lib/module/web/renderers/ListRenderers.js +7 -2
- package/lib/module/web/renderers/ListRenderers.js.map +1 -1
- package/lib/module/web/renderers/index.js +7 -4
- package/lib/module/web/renderers/index.js.map +1 -1
- package/lib/module/web/styles.js +101 -6
- package/lib/module/web/styles.js.map +1 -1
- package/lib/module/web/utils.js +14 -0
- package/lib/module/web/utils.js.map +1 -1
- package/lib/module/web/wasm/md4c.js +0 -0
- package/lib/module/web/wasm/md4c.js.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +185 -2
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts +94 -9
- package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts +69 -5
- package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +185 -2
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/headingDefaults.d.ts +9 -0
- package/lib/typescript/src/headingDefaults.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/index.web.d.ts +2 -2
- package/lib/typescript/src/index.web.d.ts.map +1 -1
- package/lib/typescript/src/jest/index.d.ts +6 -0
- package/lib/typescript/src/jest/index.d.ts.map +1 -0
- 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/normalizeMarkdownTextInputStyle.d.ts +19 -5
- package/lib/typescript/src/normalizeMarkdownTextInputStyle.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownStyle.d.ts +122 -5
- package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +39 -0
- package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownTextProps.d.ts +207 -10
- package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownTextProps.web.d.ts +60 -1
- package/lib/typescript/src/types/MarkdownTextProps.web.d.ts.map +1 -1
- package/lib/typescript/src/types/events.d.ts +85 -0
- package/lib/typescript/src/types/events.d.ts.map +1 -1
- package/lib/typescript/src/utils/textInputState.d.ts +24 -0
- package/lib/typescript/src/utils/textInputState.d.ts.map +1 -0
- 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/parseMarkdown.d.ts +1 -1
- package/lib/typescript/src/web/parseMarkdown.d.ts.map +1 -1
- package/lib/typescript/src/web/renderers/InlineRenderers.d.ts.map +1 -1
- package/lib/typescript/src/web/renderers/ListRenderers.d.ts.map +1 -1
- package/lib/typescript/src/web/renderers/index.d.ts +2 -1
- package/lib/typescript/src/web/renderers/index.d.ts.map +1 -1
- package/lib/typescript/src/web/styles.d.ts +2 -1
- package/lib/typescript/src/web/styles.d.ts.map +1 -1
- package/lib/typescript/src/web/types.d.ts +10 -2
- 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 +36 -9
- package/postinstall.mjs +131 -0
- package/ratex-version.json +21 -0
- package/src/EnrichedMarkdownNativeComponent.ts +203 -1
- package/src/EnrichedMarkdownTextInput.tsx +159 -8
- package/src/EnrichedMarkdownTextInputNativeComponent.ts +78 -5
- package/src/EnrichedMarkdownTextNativeComponent.ts +204 -1
- package/src/headingDefaults.ts +21 -0
- package/src/index.tsx +15 -0
- package/src/index.web.tsx +6 -1
- package/src/jest/index.tsx +236 -0
- package/src/native/EnrichedMarkdownText.tsx +260 -11
- package/src/normalizeMarkdownStyle.ts +117 -16
- package/src/normalizeMarkdownStyle.web.ts +76 -15
- package/src/normalizeMarkdownTextInputStyle.ts +110 -35
- package/src/normalizeMenuItem.ts +1 -1
- package/src/types/MarkdownStyle.ts +125 -5
- package/src/types/MarkdownStyleInternal.ts +51 -0
- package/src/types/MarkdownTextProps.ts +215 -10
- package/src/types/MarkdownTextProps.web.ts +62 -1
- package/src/types/events.ts +90 -0
- package/src/utils/textInputState.ts +35 -0
- package/src/web/EnrichedMarkdownText.tsx +26 -5
- package/src/web/parseMarkdown.ts +6 -2
- package/src/web/renderers/InlineRenderers.tsx +116 -3
- package/src/web/renderers/ListRenderers.tsx +14 -2
- package/src/web/renderers/index.tsx +6 -2
- package/src/web/styles.ts +114 -5
- package/src/web/types.ts +10 -1
- package/src/web/utils.ts +9 -0
- package/src/web/wasm/md4c.js +0 -0
- package/vendor-grammars.mjs +511 -0
- package/vendor-ratex.mjs +173 -0
- package/app.plugin.js +0 -2
- package/plugin/build/withAndroidMath.js +0 -19
- package/plugin/build/withIosMath.js +0 -28
- package/plugin/build/withReactNativeEnrichedMarkdown.js +0 -11
- package/plugin/src/withAndroidMath.ts +0 -24
- package/plugin/src/withIosMath.ts +0 -35
- package/plugin/src/withReactNativeEnrichedMarkdown.ts +0 -17
- package/plugin/tsconfig.build.json +0 -16
package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
package com.swmansion.enriched.markdown.input
|
|
2
2
|
|
|
3
|
-
import android.content.ClipData
|
|
4
3
|
import android.content.ClipboardManager
|
|
5
4
|
import android.content.Context
|
|
6
5
|
import android.graphics.BlendMode
|
|
@@ -20,6 +19,9 @@ import android.view.inputmethod.EditorInfo
|
|
|
20
19
|
import android.view.inputmethod.InputConnection
|
|
21
20
|
import android.view.inputmethod.InputMethodManager
|
|
22
21
|
import androidx.appcompat.widget.AppCompatEditText
|
|
22
|
+
import androidx.core.view.ContentInfoCompat
|
|
23
|
+
import androidx.core.view.OnReceiveContentListener
|
|
24
|
+
import androidx.core.view.ViewCompat
|
|
23
25
|
import com.facebook.react.common.ReactConstants
|
|
24
26
|
import com.facebook.react.uimanager.BackgroundStyleApplicator
|
|
25
27
|
import com.facebook.react.uimanager.PixelUtil
|
|
@@ -28,45 +30,55 @@ import com.facebook.react.views.text.ReactTypefaceUtils
|
|
|
28
30
|
import com.swmansion.enriched.markdown.input.autolink.AutoLinkDetector
|
|
29
31
|
import com.swmansion.enriched.markdown.input.autolink.LinkRegexConfig
|
|
30
32
|
import com.swmansion.enriched.markdown.input.detection.DetectorPipeline
|
|
31
|
-
import com.swmansion.enriched.markdown.input.
|
|
33
|
+
import com.swmansion.enriched.markdown.input.editing.BlockEditCoordinator
|
|
34
|
+
import com.swmansion.enriched.markdown.input.editing.ClipboardCoordinator
|
|
35
|
+
import com.swmansion.enriched.markdown.input.editing.EditContext
|
|
36
|
+
import com.swmansion.enriched.markdown.input.editing.EditPhase
|
|
37
|
+
import com.swmansion.enriched.markdown.input.editing.EditPipeline
|
|
38
|
+
import com.swmansion.enriched.markdown.input.editing.EditPipelineHost
|
|
39
|
+
import com.swmansion.enriched.markdown.input.editing.EditSession
|
|
32
40
|
import com.swmansion.enriched.markdown.input.editing.InputConnectionWrapper
|
|
41
|
+
import com.swmansion.enriched.markdown.input.editing.LinkCoordinator
|
|
33
42
|
import com.swmansion.enriched.markdown.input.editing.MarkdownEditableFactory
|
|
34
43
|
import com.swmansion.enriched.markdown.input.editing.MarkdownTextWatcher
|
|
44
|
+
import com.swmansion.enriched.markdown.input.editing.MentionCoordinator
|
|
45
|
+
import com.swmansion.enriched.markdown.input.editing.MentionEvent
|
|
46
|
+
import com.swmansion.enriched.markdown.input.editing.ZWSP
|
|
47
|
+
import com.swmansion.enriched.markdown.input.editing.isLineBreak
|
|
48
|
+
import com.swmansion.enriched.markdown.input.formatting.BlockStore
|
|
35
49
|
import com.swmansion.enriched.markdown.input.formatting.FormattingStore
|
|
36
50
|
import com.swmansion.enriched.markdown.input.formatting.InputFormatter
|
|
37
51
|
import com.swmansion.enriched.markdown.input.formatting.InputParser
|
|
38
52
|
import com.swmansion.enriched.markdown.input.layout.InputEventEmitter
|
|
39
53
|
import com.swmansion.enriched.markdown.input.layout.InputLayoutManager
|
|
40
54
|
import com.swmansion.enriched.markdown.input.math.MathChipController
|
|
55
|
+
import com.swmansion.enriched.markdown.input.model.BlockRange
|
|
56
|
+
import com.swmansion.enriched.markdown.input.model.BlockType
|
|
41
57
|
import com.swmansion.enriched.markdown.input.model.FormattingRange
|
|
42
58
|
import com.swmansion.enriched.markdown.input.model.InputFormatterStyle
|
|
59
|
+
import com.swmansion.enriched.markdown.input.model.MathAtom
|
|
43
60
|
import com.swmansion.enriched.markdown.input.model.MathChipStyle
|
|
61
|
+
import com.swmansion.enriched.markdown.input.model.OBJECT_REPLACEMENT
|
|
44
62
|
import com.swmansion.enriched.markdown.input.model.StyleType
|
|
45
63
|
import com.swmansion.enriched.markdown.input.toolbar.InputContextMenu
|
|
46
64
|
import com.swmansion.enriched.markdown.utils.input.AutoCapitalizeUtils
|
|
65
|
+
import com.swmansion.enriched.markdown.utils.input.PasteTextUtils
|
|
47
66
|
import kotlin.math.ceil
|
|
48
67
|
|
|
49
|
-
private fun Char.isLineBreak(): Boolean = this == '\n' || this == '\r' || this == '\u0085' || this == '\u2028' || this == '\u2029'
|
|
50
|
-
|
|
51
68
|
class EnrichedMarkdownTextInputView(
|
|
52
69
|
context: Context,
|
|
53
70
|
) : AppCompatEditText(context) {
|
|
54
71
|
private var isComponentReady = false
|
|
55
72
|
|
|
56
73
|
val formattingStore = FormattingStore()
|
|
57
|
-
val
|
|
74
|
+
val blockStore = BlockStore()
|
|
75
|
+
val formatter = InputFormatter(resources.displayMetrics.density)
|
|
58
76
|
val pendingStyles = mutableSetOf<StyleType>()
|
|
59
77
|
val pendingStyleRemovals = mutableSetOf<StyleType>()
|
|
60
78
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
var blockEmitting = false
|
|
79
|
+
val editSession = EditSession()
|
|
80
|
+
val blockCoordinator = BlockEditCoordinator(blockStore)
|
|
65
81
|
|
|
66
|
-
private var isTextChanging = false
|
|
67
|
-
var isProcessingTextChange = false
|
|
68
|
-
private set
|
|
69
|
-
private var didTextChangeRecently = false
|
|
70
82
|
private var lastProcessedText: String = ""
|
|
71
83
|
private var preEditSelectionStart = 0
|
|
72
84
|
private var preEditSelectionEnd = 0
|
|
@@ -85,18 +97,99 @@ class EnrichedMarkdownTextInputView(
|
|
|
85
97
|
val eventEmitter = InputEventEmitter(this)
|
|
86
98
|
private val autoLinkDetector = AutoLinkDetector(formattingStore)
|
|
87
99
|
private val detectorPipeline = DetectorPipeline()
|
|
100
|
+
private val mentionCoordinator = MentionCoordinator(formattingStore)
|
|
101
|
+
private val linkCoordinator = LinkCoordinator(formattingStore, autoLinkDetector)
|
|
88
102
|
|
|
89
|
-
val
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
103
|
+
private val editPipelineHost =
|
|
104
|
+
object : EditPipelineHost {
|
|
105
|
+
override val editable: Editable? get() = text
|
|
106
|
+
override val emitMarkdown: Boolean get() = this@EnrichedMarkdownTextInputView.emitMarkdown
|
|
107
|
+
|
|
108
|
+
override fun syncEmptyListAnchor(restamp: Boolean) = this@EnrichedMarkdownTextInputView.syncEmptyListAnchor(restamp)
|
|
109
|
+
|
|
110
|
+
override fun forceScrollToSelection() = this@EnrichedMarkdownTextInputView.forceScrollToSelection()
|
|
111
|
+
|
|
112
|
+
override fun syncCursorSizeWithBlock() = this@EnrichedMarkdownTextInputView.syncCursorSizeWithBlock()
|
|
113
|
+
|
|
114
|
+
override fun updateActiveMention() = this@EnrichedMarkdownTextInputView.dispatchMentionUpdate()
|
|
115
|
+
|
|
116
|
+
override fun runAsATransaction(block: () -> Unit) = this@EnrichedMarkdownTextInputView.runAsATransaction(block)
|
|
117
|
+
|
|
118
|
+
override fun setViewSelection(position: Int) = setSelection(position)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
val editPipeline =
|
|
122
|
+
EditPipeline(
|
|
123
|
+
formattingStore = formattingStore,
|
|
124
|
+
blockStore = blockStore,
|
|
125
|
+
formatter = formatter,
|
|
126
|
+
detectorPipeline = detectorPipeline,
|
|
127
|
+
eventEmitter = eventEmitter,
|
|
128
|
+
host = editPipelineHost,
|
|
129
|
+
)
|
|
94
130
|
|
|
95
131
|
private var textWatcher: MarkdownTextWatcher? = null
|
|
96
132
|
private var inputMethodManager: InputMethodManager? = null
|
|
97
133
|
private var detectScrollMovement = false
|
|
98
134
|
var scrollEnabled: Boolean = true
|
|
99
135
|
|
|
136
|
+
private val clipboardCoordinator = ClipboardCoordinator(formattingStore, blockStore, detectorPipeline, formatter)
|
|
137
|
+
|
|
138
|
+
// region Math chip state
|
|
139
|
+
//
|
|
140
|
+
// Null when the build has math disabled (the `src/math/java` source set is excluded,
|
|
141
|
+
// so `resolve()` finds no implementation). Every math path below returns early on
|
|
142
|
+
// null, which is what makes a math-disabled build a silent no-op instead of a crash.
|
|
143
|
+
val mathChipController: MathChipController? = MathChipController.resolve(context)
|
|
144
|
+
|
|
145
|
+
// The chip spans hold a resolved style object, so the view has to keep the last
|
|
146
|
+
// `markdownStyle` around to re-stamp them; InputFormatter keeps its copy private.
|
|
147
|
+
private var inputStyle: InputFormatterStyle? = null
|
|
148
|
+
|
|
149
|
+
// Chip whose selected styling is currently drawn, or -1. Kept so the caret leaving
|
|
150
|
+
// the chip can clear it — a span cannot observe selection changes itself.
|
|
151
|
+
private var activeMathChipPosition = -1
|
|
152
|
+
|
|
153
|
+
// Atoms parsed before `markdownStyle` arrived. @ReactProp setters fire in undefined
|
|
154
|
+
// order, so `defaultValue` can land first; they are drained by the style setter.
|
|
155
|
+
private var pendingMathAtoms: List<MathAtom> = emptyList()
|
|
156
|
+
|
|
157
|
+
private var lastReportedBlockWidth = -1
|
|
158
|
+
// endregion
|
|
159
|
+
|
|
160
|
+
private var headingOverrideBaseSizePx: Float? = null
|
|
161
|
+
private var baseHintColor: Int? = null
|
|
162
|
+
|
|
163
|
+
// Number of ZWSP empty-list anchors believed live in the buffer. Bumped on insert,
|
|
164
|
+
// made exact on every strip scan (which recounts what it keeps), and reset when the
|
|
165
|
+
// buffer is replaced. Incoming markdown is scrubbed of U+200B at parse time, so in
|
|
166
|
+
// practice only syncEmptyListAnchor's own inserts raise it. When it is 0 there is
|
|
167
|
+
// nothing to strip, letting syncEmptyListAnchor skip its O(document) backward scan
|
|
168
|
+
// on every caret move.
|
|
169
|
+
private var zwspAnchorCount = 0
|
|
170
|
+
|
|
171
|
+
// The consumer-set placeholder, hidden while a bullet is drawn on an empty editor.
|
|
172
|
+
private var userHint: CharSequence? = null
|
|
173
|
+
|
|
174
|
+
// When on, a paste is reported to JS instead of inserted. Native never touches
|
|
175
|
+
// the buffer here (see resolvePaste): if JS dies mid-handler the paste is a
|
|
176
|
+
// visible no-op rather than a selection that silently vanished.
|
|
177
|
+
//
|
|
178
|
+
// The listener is installed only while the prop is on. Registering it
|
|
179
|
+
// unconditionally would route every consumer's paste through AppCompat's
|
|
180
|
+
// receive-content path even with the feature off, which is a behavior change
|
|
181
|
+
// nobody asked for.
|
|
182
|
+
var interceptPaste: Boolean = false
|
|
183
|
+
set(value) {
|
|
184
|
+
if (field == value) return
|
|
185
|
+
field = value
|
|
186
|
+
ViewCompat.setOnReceiveContentListener(
|
|
187
|
+
this,
|
|
188
|
+
if (value) PASTE_MIME_TYPES else null,
|
|
189
|
+
if (value) receiveContentListener else null,
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
100
193
|
// The markdown last handed over by JS, kept so `blockSyntax` can re-parse it. @ReactProp
|
|
101
194
|
// setters fire in undefined order, so `defaultValue` regularly lands before the flag does.
|
|
102
195
|
private var lastMarkdownFromJS: String? = null
|
|
@@ -108,12 +201,6 @@ class EnrichedMarkdownTextInputView(
|
|
|
108
201
|
lastMarkdownFromJS?.let { setValueFromJS(it) }
|
|
109
202
|
}
|
|
110
203
|
|
|
111
|
-
private var mentionIndicators: LinkedHashSet<String> = linkedSetOf()
|
|
112
|
-
private var activeMentionIndicator: String? = null
|
|
113
|
-
private var activeMentionStart = -1
|
|
114
|
-
private var activeMentionEnd = -1
|
|
115
|
-
private var activeMentionText = ""
|
|
116
|
-
|
|
117
204
|
init {
|
|
118
205
|
setupDetectorPipeline()
|
|
119
206
|
prepareComponent()
|
|
@@ -121,16 +208,6 @@ class EnrichedMarkdownTextInputView(
|
|
|
121
208
|
isComponentReady = true
|
|
122
209
|
}
|
|
123
210
|
|
|
124
|
-
/**
|
|
125
|
-
* Width a full-bleed block math chip can claim. Read late (every layout pass),
|
|
126
|
-
* because TextView's width is established after measure. Falls back to 0 when
|
|
127
|
-
* we haven't been measured yet — span will then use its intrinsic width.
|
|
128
|
-
*/
|
|
129
|
-
private fun availableBlockWidth(): Int {
|
|
130
|
-
val w = width - totalPaddingLeft - totalPaddingRight
|
|
131
|
-
return w.coerceAtLeast(0)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
211
|
private fun setupDetectorPipeline() {
|
|
135
212
|
autoLinkDetector.onLinkDetected = { text, url, start, end ->
|
|
136
213
|
eventEmitter.emitLinkDetected(text, url, start, end)
|
|
@@ -175,6 +252,13 @@ class EnrichedMarkdownTextInputView(
|
|
|
175
252
|
runAsATransaction { super.setTextIsSelectable(true) }
|
|
176
253
|
}
|
|
177
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Width a full-bleed block math chip may claim. Read late (on every layout pass)
|
|
257
|
+
* because a TextView's width is only known after measure; 0 before the first
|
|
258
|
+
* measure, which makes the span fall back to its intrinsic width.
|
|
259
|
+
*/
|
|
260
|
+
private fun availableBlockWidth(): Int = (width - totalPaddingLeft - totalPaddingRight).coerceAtLeast(0)
|
|
261
|
+
|
|
178
262
|
override fun onSizeChanged(
|
|
179
263
|
w: Int,
|
|
180
264
|
h: Int,
|
|
@@ -186,9 +270,9 @@ class EnrichedMarkdownTextInputView(
|
|
|
186
270
|
val available = availableBlockWidth()
|
|
187
271
|
if (available == lastReportedBlockWidth) return
|
|
188
272
|
lastReportedBlockWidth = available
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
273
|
+
// The first measure (oldw == 0) is where block chips first get a real width, so
|
|
274
|
+
// their cached layout has to be dropped for getSize to report the new full-bleed
|
|
275
|
+
// width. Rotation and resize land here too.
|
|
192
276
|
refreshAllMathChips()
|
|
193
277
|
}
|
|
194
278
|
|
|
@@ -209,16 +293,62 @@ class EnrichedMarkdownTextInputView(
|
|
|
209
293
|
return InputConnectionWrapper(base, this)
|
|
210
294
|
}
|
|
211
295
|
|
|
296
|
+
// Plain Tab is consumed before the platform's focus navigation so it inserts
|
|
297
|
+
// a tab character and emits onKeyPress, matching iOS. Shift/Ctrl+Tab still
|
|
298
|
+
// navigate focus.
|
|
212
299
|
override fun onKeyDown(
|
|
213
300
|
keyCode: Int,
|
|
214
301
|
event: KeyEvent?,
|
|
215
302
|
): Boolean {
|
|
303
|
+
if (keyCode == KeyEvent.KEYCODE_TAB && event?.hasNoModifiers() != false) {
|
|
304
|
+
eventEmitter.emitKeyPress("Tab")
|
|
305
|
+
val start = minOf(selectionStart, selectionEnd).coerceAtLeast(0)
|
|
306
|
+
val end = maxOf(selectionStart, selectionEnd).coerceAtLeast(0)
|
|
307
|
+
text?.replace(start, end, "\t")
|
|
308
|
+
return true
|
|
309
|
+
}
|
|
216
310
|
if (keyCode == KeyEvent.KEYCODE_DEL && deleteLinkBeforeCursor()) {
|
|
217
311
|
return true
|
|
218
312
|
}
|
|
313
|
+
if (handleListKey(keyCode, event)) {
|
|
314
|
+
return true
|
|
315
|
+
}
|
|
219
316
|
return super.onKeyDown(keyCode, event)
|
|
220
317
|
}
|
|
221
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Hardware-keyboard list editing: Tab indents the current item, Shift+Tab outdents,
|
|
321
|
+
* and Backspace at the start of an item (or on an empty/ZWSP-anchored item) outdents,
|
|
322
|
+
* then un-lists at depth 0. Only fires on a list line; returns true when handled.
|
|
323
|
+
*/
|
|
324
|
+
private fun handleListKey(
|
|
325
|
+
keyCode: Int,
|
|
326
|
+
event: KeyEvent?,
|
|
327
|
+
): Boolean {
|
|
328
|
+
val listBlock = listBlockAtCursor() ?: return false
|
|
329
|
+
val depth = listBlock.level
|
|
330
|
+
when (keyCode) {
|
|
331
|
+
KeyEvent.KEYCODE_TAB -> {
|
|
332
|
+
if (event?.isShiftPressed == true) outdentList() else indentList()
|
|
333
|
+
return true
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
KeyEvent.KEYCODE_DEL -> {
|
|
337
|
+
if (selectionStart == selectionEnd) {
|
|
338
|
+
val editable = text ?: return false
|
|
339
|
+
val ls = blockCoordinator.lineStartOf(editable, selectionStart)
|
|
340
|
+
val le = blockCoordinator.lineEndOf(editable, selectionStart)
|
|
341
|
+
val content = editable.subSequence(ls, le).toString()
|
|
342
|
+
if (selectionStart == ls || content.isEmpty() || content == ZWSP.toString()) {
|
|
343
|
+
if (depth > 0) outdentList() else toggleListType(listBlock.type)
|
|
344
|
+
return true
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return false
|
|
350
|
+
}
|
|
351
|
+
|
|
222
352
|
// Prevents TextView from deferring its internal layout when a Fabric
|
|
223
353
|
// state-update (height change) triggers requestLayout(). Without this
|
|
224
354
|
// override the deferred relayout causes a visible flicker of styled spans.
|
|
@@ -245,8 +375,15 @@ class EnrichedMarkdownTextInputView(
|
|
|
245
375
|
|
|
246
376
|
MotionEvent.ACTION_UP -> {
|
|
247
377
|
val chipPosition = mathChipPositionAtTouch(ev.x, ev.y)
|
|
248
|
-
if (chipPosition >= 0
|
|
249
|
-
|
|
378
|
+
if (chipPosition >= 0) {
|
|
379
|
+
// super FIRST, then the chip handling on top of it. ACTION_UP is where View
|
|
380
|
+
// takes focus and where the editor shows the soft keyboard; consuming it
|
|
381
|
+
// outright would make the first tap on a chip in an unfocused editor open
|
|
382
|
+
// nothing — the Android twin of the iOS tap-to-focus stall fixed in 475ecdab.
|
|
383
|
+
// handleMathChipTap re-seats the caret afterwards, overriding the hit-test
|
|
384
|
+
// position super just set.
|
|
385
|
+
val handledBySuper = super.onTouchEvent(ev)
|
|
386
|
+
return handleMathChipTap(chipPosition) || handledBySuper
|
|
250
387
|
}
|
|
251
388
|
}
|
|
252
389
|
}
|
|
@@ -261,6 +398,10 @@ class EnrichedMarkdownTextInputView(
|
|
|
261
398
|
x: Int,
|
|
262
399
|
y: Int,
|
|
263
400
|
) {
|
|
401
|
+
if (text.isNullOrEmpty()) {
|
|
402
|
+
super.scrollTo(x, 0)
|
|
403
|
+
return
|
|
404
|
+
}
|
|
264
405
|
if (!scrollEnabled) return
|
|
265
406
|
super.scrollTo(x, y)
|
|
266
407
|
}
|
|
@@ -278,17 +419,16 @@ class EnrichedMarkdownTextInputView(
|
|
|
278
419
|
}
|
|
279
420
|
|
|
280
421
|
fun runAsATransaction(block: () -> Unit) {
|
|
281
|
-
|
|
282
|
-
isDuringTransaction = true
|
|
422
|
+
if (editSession.phase != EditPhase.Idle) {
|
|
283
423
|
block()
|
|
284
|
-
}
|
|
285
|
-
|
|
424
|
+
} else {
|
|
425
|
+
editSession.scoped(EditPhase.Processing) { block() }
|
|
286
426
|
}
|
|
287
427
|
}
|
|
288
428
|
|
|
289
429
|
fun onBeforeTextChanged() {
|
|
290
|
-
if (
|
|
291
|
-
isTextChanging = true
|
|
430
|
+
if (editSession.phase != EditPhase.Idle) return
|
|
431
|
+
editSession.isTextChanging = true
|
|
292
432
|
preEditSelectionStart = selectionStart
|
|
293
433
|
preEditSelectionEnd = selectionEnd
|
|
294
434
|
}
|
|
@@ -298,32 +438,30 @@ class EnrichedMarkdownTextInputView(
|
|
|
298
438
|
deletedLength: Int,
|
|
299
439
|
insertedLength: Int,
|
|
300
440
|
) {
|
|
301
|
-
if (
|
|
441
|
+
if (editSession.phase != EditPhase.Idle) return
|
|
302
442
|
|
|
303
443
|
val currentText = text?.toString() ?: ""
|
|
304
444
|
if (currentText == lastProcessedText) return
|
|
305
445
|
|
|
306
|
-
|
|
446
|
+
editSession.enter(EditPhase.Processing)
|
|
307
447
|
try {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
didTextChangeRecently = true
|
|
324
|
-
lastProcessedText = currentText
|
|
448
|
+
val context =
|
|
449
|
+
EditContext(
|
|
450
|
+
editStart = editStart,
|
|
451
|
+
deletedLength = deletedLength,
|
|
452
|
+
insertedLength = insertedLength,
|
|
453
|
+
preEditText = currentText,
|
|
454
|
+
preEditSelectionStart = preEditSelectionStart,
|
|
455
|
+
preEditSelectionEnd = preEditSelectionEnd,
|
|
456
|
+
pendingStyles = pendingStyles.toSet(),
|
|
457
|
+
pendingStyleRemovals = pendingStyleRemovals.toSet(),
|
|
458
|
+
)
|
|
459
|
+
editPipeline.processTextChange(context)
|
|
460
|
+
editSession.isTextChanging = false
|
|
461
|
+
editSession.didTextChangeRecently = true
|
|
462
|
+
lastProcessedText = text?.toString() ?: currentText
|
|
325
463
|
} finally {
|
|
326
|
-
|
|
464
|
+
editSession.exit()
|
|
327
465
|
}
|
|
328
466
|
}
|
|
329
467
|
|
|
@@ -332,70 +470,107 @@ class EnrichedMarkdownTextInputView(
|
|
|
332
470
|
selEnd: Int,
|
|
333
471
|
) {
|
|
334
472
|
super.onSelectionChanged(selStart, selEnd)
|
|
335
|
-
if (!isComponentReady ||
|
|
473
|
+
if (!isComponentReady || editSession.shouldSuppressTextWatcher) return
|
|
336
474
|
|
|
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.
|
|
475
|
+
if (!editSession.isTextChanging) {
|
|
340
476
|
formattingStore.selectionAdjustedForAtomicLinks(selStart, selEnd)?.let { (newStart, newEnd) ->
|
|
341
477
|
setSelection(newStart, newEnd)
|
|
342
478
|
return
|
|
343
479
|
}
|
|
344
|
-
if (didTextChangeRecently) {
|
|
345
|
-
didTextChangeRecently = false
|
|
480
|
+
if (editSession.didTextChangeRecently) {
|
|
481
|
+
editSession.didTextChangeRecently = false
|
|
346
482
|
} else {
|
|
347
483
|
pendingStyles.clear()
|
|
348
484
|
pendingStyleRemovals.clear()
|
|
485
|
+
seedPendingStylesFromSelection(selStart, selEnd)
|
|
349
486
|
}
|
|
350
487
|
}
|
|
351
488
|
|
|
352
|
-
|
|
489
|
+
if (!editSession.isTextChanging && editSession.phase == EditPhase.Idle) {
|
|
490
|
+
// The caret moving on/off an empty bullet line toggles the ZWSP anchor and the
|
|
491
|
+
// placeholder visibility; skip during a text-change pass (handled there).
|
|
492
|
+
syncEmptyListAnchor()
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// A chip stays highlighted only while the caret sits on either of its two edges;
|
|
496
|
+
// any other caret move (or a real selection) drops the highlight.
|
|
497
|
+
val isCaretOnActiveChip =
|
|
353
498
|
selStart == selEnd &&
|
|
354
499
|
activeMathChipPosition >= 0 &&
|
|
355
500
|
(selStart == activeMathChipPosition || selStart == activeMathChipPosition + 1)
|
|
356
|
-
if (!
|
|
501
|
+
if (!isCaretOnActiveChip) {
|
|
357
502
|
clearMathChipSelections()
|
|
358
503
|
activeMathChipPosition = -1
|
|
359
504
|
}
|
|
360
505
|
|
|
361
506
|
eventEmitter.emitSelection(selStart, selEnd)
|
|
362
|
-
|
|
507
|
+
dispatchMentionUpdate()
|
|
363
508
|
eventEmitter.emitState()
|
|
364
509
|
eventEmitter.emitCaretRectChangeIfNeeded()
|
|
365
510
|
}
|
|
366
511
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
512
|
+
/**
|
|
513
|
+
* Text typed over a non-empty selection inherits the inline styles of the first
|
|
514
|
+
* selected character (mirrors iOS rebuildFromContext and the range-inheritance
|
|
515
|
+
* rule in [com.swmansion.enriched.markdown.input.formatting.RangeEditAdjustment]).
|
|
516
|
+
* Seeding here is the only chance to carry the style through the whole typed
|
|
517
|
+
* run: the post-edit grace period above skips reseeding between keystrokes.
|
|
518
|
+
* LINK is excluded — typing over a selected link replaces it, not extends it.
|
|
519
|
+
*/
|
|
520
|
+
private fun seedPendingStylesFromSelection(
|
|
521
|
+
selStart: Int,
|
|
522
|
+
selEnd: Int,
|
|
370
523
|
) {
|
|
371
|
-
if (
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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))
|
|
524
|
+
if (selStart == selEnd) return
|
|
525
|
+
for (style in StyleType.entries) {
|
|
526
|
+
if (style == StyleType.LINK) continue
|
|
527
|
+
if (formattingStore.isStyleActive(style, selStart)) {
|
|
528
|
+
pendingStyles.add(style)
|
|
388
529
|
}
|
|
389
530
|
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Adjusts both [formattingStore] and [blockStore] for a text edit, then prunes
|
|
535
|
+
* orphaned anchors and normalizes block ranges to line bounds. Every code path
|
|
536
|
+
* that mutates the text buffer must call this so block ranges stay in sync —
|
|
537
|
+
* mirrors iOS's `replaceTextInRange:withText:formattingRanges:blockRanges:`.
|
|
538
|
+
*/
|
|
539
|
+
private fun adjustStoresForEdit(
|
|
540
|
+
editStart: Int,
|
|
541
|
+
deletedLength: Int,
|
|
542
|
+
insertedLength: Int,
|
|
543
|
+
) {
|
|
544
|
+
formattingStore.adjustForEdit(editStart, deletedLength, insertedLength)
|
|
545
|
+
blockStore.adjustForEdit(editStart, deletedLength, insertedLength)
|
|
546
|
+
editPipeline.pruneOrphanedAnchors()
|
|
547
|
+
text?.let { blockStore.normalizeToLineBounds(it) }
|
|
548
|
+
}
|
|
390
549
|
|
|
391
|
-
|
|
392
|
-
|
|
550
|
+
private inline fun replaceTextInRange(
|
|
551
|
+
start: Int,
|
|
552
|
+
end: Int,
|
|
553
|
+
newText: String,
|
|
554
|
+
postAdjust: (Editable) -> Unit = {},
|
|
555
|
+
) {
|
|
556
|
+
val editable = text ?: return
|
|
557
|
+
editSession.enter(EditPhase.Processing)
|
|
558
|
+
try {
|
|
559
|
+
editable.replace(start, end, newText)
|
|
560
|
+
adjustStoresForEdit(start, end - start, newText.length)
|
|
561
|
+
postAdjust(editable)
|
|
562
|
+
lastProcessedText = editable.toString()
|
|
563
|
+
applyFormattingAndEmit()
|
|
564
|
+
eventEmitter.emitChangeText()
|
|
565
|
+
} finally {
|
|
566
|
+
editSession.exit()
|
|
393
567
|
}
|
|
394
568
|
}
|
|
395
569
|
|
|
396
570
|
fun applyFormatting() {
|
|
397
571
|
val editable = text ?: return
|
|
398
572
|
formatter.applyFormatting(editable, formattingStore.allRanges)
|
|
573
|
+
formatter.applyBlockFormatting(editable, blockStore.allRanges)
|
|
399
574
|
}
|
|
400
575
|
|
|
401
576
|
private fun applyFormattingAndEmit() {
|
|
@@ -406,6 +581,11 @@ class EnrichedMarkdownTextInputView(
|
|
|
406
581
|
}
|
|
407
582
|
|
|
408
583
|
private fun forceScrollToSelection() {
|
|
584
|
+
if (text.isNullOrEmpty()) {
|
|
585
|
+
super.scrollTo(scrollX, 0)
|
|
586
|
+
return
|
|
587
|
+
}
|
|
588
|
+
|
|
409
589
|
val textLayout = layout ?: return
|
|
410
590
|
val cursorOffset = selectionStart
|
|
411
591
|
if (cursorOffset <= 0) return
|
|
@@ -434,21 +614,12 @@ class EnrichedMarkdownTextInputView(
|
|
|
434
614
|
fun toggleInlineStyle(styleType: StyleType) {
|
|
435
615
|
val handler = formatter.handlers[styleType] ?: return
|
|
436
616
|
val mergingConfig = handler.mergingConfig
|
|
437
|
-
|
|
438
617
|
val selStart = selectionStart
|
|
439
618
|
val selEnd = selectionEnd
|
|
440
619
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
if (!isCurrentlyActive) {
|
|
445
|
-
for (blocker in mergingConfig.blockingStyles) {
|
|
446
|
-
if (formattingStore.isStyleActive(blocker, selStart)) {
|
|
447
|
-
return
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
620
|
+
if (formattingStore.isToggleBlocked(styleType, selStart, mergingConfig.blockingStyles)) return
|
|
621
|
+
|
|
622
|
+
val result = formattingStore.toggleStyle(styleType, selStart, selEnd, mergingConfig.conflictingStyles)
|
|
452
623
|
|
|
453
624
|
if (selStart == selEnd) {
|
|
454
625
|
if (pendingStyleRemovals.contains(styleType)) {
|
|
@@ -457,72 +628,441 @@ class EnrichedMarkdownTextInputView(
|
|
|
457
628
|
} else if (pendingStyles.contains(styleType)) {
|
|
458
629
|
pendingStyles.remove(styleType)
|
|
459
630
|
pendingStyleRemovals.add(styleType)
|
|
460
|
-
} else if (
|
|
631
|
+
} else if (result == FormattingStore.ToggleResult.WAS_ACTIVE) {
|
|
461
632
|
pendingStyleRemovals.add(styleType)
|
|
462
633
|
} else {
|
|
463
634
|
pendingStyles.add(styleType)
|
|
464
635
|
}
|
|
465
636
|
eventEmitter.emitState()
|
|
466
637
|
} else {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
638
|
+
applyFormattingAndEmit()
|
|
639
|
+
pendingStyles.clear()
|
|
640
|
+
pendingStyleRemovals.clear()
|
|
641
|
+
seedPendingStylesFromSelection(selStart, selEnd)
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
private fun listBlockAtCursor(): BlockRange? {
|
|
646
|
+
val editable = text ?: return null
|
|
647
|
+
return blockCoordinator.listBlockAtPosition(editable, selectionStart)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
fun listStateAtCursor(type: BlockType): Pair<Boolean, Int> {
|
|
651
|
+
val editable = text ?: return false to 0
|
|
652
|
+
return blockCoordinator.listStateAtPosition(editable, selectionStart, type)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
fun toggleUnorderedList() = toggleListType(BlockType.UNORDERED_LIST_ITEM)
|
|
656
|
+
|
|
657
|
+
fun toggleOrderedList() = toggleListType(BlockType.ORDERED_LIST_ITEM)
|
|
658
|
+
|
|
659
|
+
private fun toggleListType(type: BlockType) {
|
|
660
|
+
val editable = text ?: return
|
|
661
|
+
blockCoordinator.toggleList(editable, type, selectionStart, selectionStart, selectionEnd)
|
|
662
|
+
applyFormattingAndEmit()
|
|
663
|
+
syncEmptyListAnchor()
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** Increases the nesting depth of the selected list item(s). QoL: indenting a plain paragraph starts a list. */
|
|
667
|
+
fun indentList() = changeListDepthBy(1)
|
|
668
|
+
|
|
669
|
+
/** Decreases the nesting depth; outdenting at depth 0 removes the list marker. */
|
|
670
|
+
fun outdentList() = changeListDepthBy(-1)
|
|
671
|
+
|
|
672
|
+
private fun changeListDepthBy(delta: Int) {
|
|
673
|
+
val editable = text ?: return
|
|
674
|
+
val result = blockCoordinator.changeDepth(editable, selectionStart, selectionStart, selectionEnd, delta)
|
|
675
|
+
if (result == BlockEditCoordinator.DepthChangeResult.NO_OP) return
|
|
676
|
+
applyFormattingAndEmit()
|
|
677
|
+
syncEmptyListAnchor()
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Keeps an empty bullet line anchored by a ZWSP so its marker draws and the caret
|
|
682
|
+
* indents (a [android.text.style.LeadingMarginSpan] doesn't indent an empty
|
|
683
|
+
* paragraph). Inserts the ZWSP on the caret's empty list line, strips stale ones.
|
|
684
|
+
*
|
|
685
|
+
* @param restamp re-apply block formatting here (selection/command paths); the
|
|
686
|
+
* text-change pass passes false and stamps once afterwards.
|
|
687
|
+
* @return true if an anchor was inserted or stripped (text/ranges mutated).
|
|
688
|
+
*
|
|
689
|
+
* When called from `onAfterTextChanged` this mutates the [Editable] from inside
|
|
690
|
+
* the text-change callback — normally an Android hazard around IME composition,
|
|
691
|
+
* undo/redo, and accessibility. It is safe here, and deliberately synchronous,
|
|
692
|
+
* because: every buffer mutation is wrapped in [runAsATransaction] (which keeps
|
|
693
|
+
* [EditSession.phase] non-idle, so [MarkdownTextWatcher] early-returns and the edit
|
|
694
|
+
* does not re-enter this pass), the whole `onAfterTextChanged` body holds
|
|
695
|
+
* [EditPhase.Processing], and [EditPhase.ManagingAnchors] blocks re-entry via the
|
|
696
|
+
* selection-change path. The mutation must stay synchronous: the single-stamp
|
|
697
|
+
* ordering above, the caret placement past the anchor, and `lastProcessedText`
|
|
698
|
+
* all depend on the buffer reaching its final state within this same callback —
|
|
699
|
+
* deferring the insert/delete (e.g. via `post`) would run it outside every guard
|
|
700
|
+
* and reintroduce the double-bullet and stale-caret bugs.
|
|
701
|
+
*/
|
|
702
|
+
private fun syncEmptyListAnchor(restamp: Boolean = true): Boolean {
|
|
703
|
+
if (editSession.shouldSuppressAnchorSync) return false
|
|
704
|
+
val editable = text ?: return false
|
|
705
|
+
editSession.enter(EditPhase.ManagingAnchors)
|
|
706
|
+
var anchorChanged = false
|
|
707
|
+
try {
|
|
708
|
+
// Strip every stale ZWSP first (a line that gained content or stopped being a
|
|
709
|
+
// list). Skip the full-document scan when we've inserted no anchors — there is
|
|
710
|
+
// nothing to strip, so a caret move in a document with no empty list lines is
|
|
711
|
+
// O(1) instead of O(document length). When the scan does run it recounts the
|
|
712
|
+
// anchors it keeps, so the count is exact afterwards — a prior overcount (an
|
|
713
|
+
// anchor the user deleted directly) or an undercount (a foreign ZWSP that stole
|
|
714
|
+
// a decrement) self-heals rather than disabling the fast path or leaking a
|
|
715
|
+
// stale anchor.
|
|
716
|
+
if (zwspAnchorCount > 0) {
|
|
717
|
+
var keptAnchors = 0
|
|
718
|
+
var i = editable.length - 1
|
|
719
|
+
while (i >= 0) {
|
|
720
|
+
if (editable[i] == ZWSP) {
|
|
721
|
+
val ls = blockCoordinator.lineStartOf(editable, i)
|
|
722
|
+
val le = blockCoordinator.lineEndOf(editable, i)
|
|
723
|
+
val onlyZwsp = le - ls == 1 && editable[ls] == ZWSP
|
|
724
|
+
val isEmptyListLine = onlyZwsp && blockCoordinator.listBlockAtLineStart(ls) != null
|
|
725
|
+
if (!isEmptyListLine) {
|
|
726
|
+
runAsATransaction { editable.delete(i, i + 1) }
|
|
727
|
+
blockStore.adjustForEdit(i, 1, 0)
|
|
728
|
+
anchorChanged = true
|
|
729
|
+
} else {
|
|
730
|
+
keptAnchors++
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
i--
|
|
474
734
|
}
|
|
475
|
-
|
|
735
|
+
zwspAnchorCount = keptAnchors
|
|
476
736
|
}
|
|
477
|
-
|
|
737
|
+
|
|
738
|
+
val caret = selectionStart
|
|
739
|
+
if (selectionStart == selectionEnd) {
|
|
740
|
+
val ls = blockCoordinator.lineStartOf(editable, caret)
|
|
741
|
+
val le = blockCoordinator.lineEndOf(editable, caret)
|
|
742
|
+
val block = blockCoordinator.listBlockAtLineStart(ls)
|
|
743
|
+
if (block != null && le == ls) {
|
|
744
|
+
runAsATransaction { editable.insert(ls, ZWSP.toString()) }
|
|
745
|
+
blockStore.adjustForEdit(ls, 0, 1)
|
|
746
|
+
blockStore.normalizeToLineBounds(editable)
|
|
747
|
+
setSelection(ls + 1)
|
|
748
|
+
zwspAnchorCount++
|
|
749
|
+
anchorChanged = true
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (anchorChanged) {
|
|
754
|
+
// Re-snap any range left zero-length by a strip so the next stamp is exact.
|
|
755
|
+
blockStore.normalizeToLineBounds(editable)
|
|
756
|
+
if (restamp) applyFormatting()
|
|
757
|
+
lastProcessedText = editable.toString()
|
|
758
|
+
if (emitMarkdown) eventEmitter.emitChangeMarkdown()
|
|
759
|
+
}
|
|
760
|
+
syncHintVisibility()
|
|
761
|
+
return anchorChanged
|
|
762
|
+
} finally {
|
|
763
|
+
editSession.exit()
|
|
478
764
|
}
|
|
479
765
|
}
|
|
480
766
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
767
|
+
/**
|
|
768
|
+
* The hint shows only on a truly empty editor with no block range — a bullet's
|
|
769
|
+
* ZWSP anchor counts as content, so the hint never overlaps a marker. Mirrors iOS.
|
|
770
|
+
*/
|
|
771
|
+
private fun syncHintVisibility() {
|
|
772
|
+
val content = text
|
|
773
|
+
val hasRealText = content != null && content.any { it != ZWSP }
|
|
774
|
+
val hasBlock = blockStore.allRanges.isNotEmpty()
|
|
775
|
+
val target: CharSequence? = if (hasRealText || hasBlock) "" else userHint
|
|
776
|
+
if (hint != target) super.setHint(target)
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
fun setUserHint(value: CharSequence?) {
|
|
780
|
+
userHint = value
|
|
781
|
+
syncHintVisibility()
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Copies the whole input as markdown without disturbing the current selection,
|
|
785
|
+
// tagged so paste restores formatting and block ranges — mirrors iOS, which
|
|
786
|
+
// stores markdown under its custom pasteboard type.
|
|
484
787
|
fun copyToClipboard() {
|
|
485
788
|
val content = text
|
|
486
789
|
if (content.isNullOrEmpty()) return
|
|
487
790
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager ?: return
|
|
488
|
-
|
|
791
|
+
val markdown = clipboardCoordinator.serializeFullDocument(content.toString(), content, mathChipsForSerialization())
|
|
792
|
+
clipboard.setPrimaryClip(MarkdownClipboard.newMarkdownClip(markdown, content.toString()))
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Copy/cut re-tag the clip with markdown and paste restores it, so formatting
|
|
796
|
+
// and block ranges survive the round trip — mirrors iOS's copy:/cut:/paste:
|
|
797
|
+
// overrides. External clips keep default handling.
|
|
798
|
+
//
|
|
799
|
+
// Paste arrives by three different routes and only the first one is a menu.
|
|
800
|
+
// This override covers the context menu and the hardware Ctrl+V /
|
|
801
|
+
// KEYCODE_PASTE shortcuts; drag-and-drop, autofill and IME `commitContent`
|
|
802
|
+
// reach the view through `onReceiveContent` instead, and a keyboard
|
|
803
|
+
// clipboard chip commits the text through the input connection (see
|
|
804
|
+
// interceptImePaste). Hooking only some of them leaves the others pasting
|
|
805
|
+
// straight into the buffer with nobody intercepting.
|
|
806
|
+
override fun onTextContextMenuItem(id: Int): Boolean {
|
|
807
|
+
// Checked before the markdown paste below, which would otherwise insert first.
|
|
808
|
+
if (interceptPaste && (id == android.R.id.paste || id == android.R.id.pasteAsPlainText)) {
|
|
809
|
+
val text = readClipboardText()
|
|
810
|
+
if (text.isNotEmpty()) {
|
|
811
|
+
eventEmitter.emitPaste(text, "")
|
|
812
|
+
// Returning without super keeps this single-emit: AppCompat would route
|
|
813
|
+
// its own paste through the receive-content listener below.
|
|
814
|
+
return true
|
|
815
|
+
}
|
|
816
|
+
// Nothing textual in the clip: fall through to the regular handling,
|
|
817
|
+
// matching what the listener does with the items it does not claim.
|
|
818
|
+
}
|
|
819
|
+
if (id == android.R.id.paste) {
|
|
820
|
+
MarkdownClipboard.markdownFromClipboard(context)?.let { markdown ->
|
|
821
|
+
pasteMarkdown(markdown)
|
|
822
|
+
return true
|
|
823
|
+
}
|
|
824
|
+
// External plain text is treated as markdown so pasted syntax ("- ", "#",
|
|
825
|
+
// "**") formats instead of landing literal; syntax-free text is unchanged.
|
|
826
|
+
// "Paste as plain text" (pasteAsPlainText) keeps the literal default.
|
|
827
|
+
plainTextFromClipboard()?.let { plainText ->
|
|
828
|
+
pasteMarkdown(plainText)
|
|
829
|
+
return true
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (id == android.R.id.copy || id == android.R.id.cut) {
|
|
833
|
+
val selStart = selectionStart
|
|
834
|
+
val selEnd = selectionEnd
|
|
835
|
+
val plainText = if (selStart < selEnd) text?.substring(selStart, selEnd) else null
|
|
836
|
+
val markdown = markdownForSelectedRange()
|
|
837
|
+
val handled = super.onTextContextMenuItem(id)
|
|
838
|
+
if (handled && markdown != null && plainText != null) {
|
|
839
|
+
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
|
840
|
+
clipboard?.setPrimaryClip(MarkdownClipboard.newMarkdownClip(markdown, plainText))
|
|
841
|
+
}
|
|
842
|
+
return handled
|
|
843
|
+
}
|
|
844
|
+
return super.onTextContextMenuItem(id)
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
fun markdownForSelectedRange(): String? {
|
|
848
|
+
val fullText = text?.toString() ?: return null
|
|
849
|
+
return clipboardCoordinator.serializeSelectedRange(
|
|
850
|
+
fullText,
|
|
851
|
+
selectionStart,
|
|
852
|
+
selectionEnd,
|
|
853
|
+
text,
|
|
854
|
+
mathChipsForSerialization(),
|
|
855
|
+
)
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
private fun plainTextFromClipboard(): String? {
|
|
859
|
+
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager ?: return null
|
|
860
|
+
val item = clipboard.primaryClip?.takeIf { it.itemCount > 0 }?.getItemAt(0) ?: return null
|
|
861
|
+
return item.coerceToText(context)?.toString()?.takeIf { it.isNotEmpty() }
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Replaces the selection with parsed markdown, importing its inline formatting
|
|
866
|
+
* and block ranges (headings etc.) into the stores — mirrors iOS pasteMarkdown.
|
|
867
|
+
*
|
|
868
|
+
* Insertion is literal: the string's characters land at the cursor exactly as
|
|
869
|
+
* given. Markdown parsing consumes leading and trailing newlines as block
|
|
870
|
+
* structure, so they are split off before the parse and re-attached verbatim;
|
|
871
|
+
* without this, insertText("\n- item\n") mid-paragraph would merge the list
|
|
872
|
+
* line with the surrounding text. Callers decide separation by including (or
|
|
873
|
+
* omitting) newlines in the string.
|
|
874
|
+
*/
|
|
875
|
+
fun pasteMarkdown(markdown: String) {
|
|
876
|
+
val editable = text ?: return
|
|
877
|
+
val prefix = markdown.takeWhile { it == '\n' }
|
|
878
|
+
val suffix = if (markdown.length > prefix.length) markdown.takeLastWhile { it == '\n' } else ""
|
|
879
|
+
val core = markdown.substring(prefix.length, markdown.length - suffix.length)
|
|
880
|
+
val parsed = InputParser.parseToPlainTextAndRanges(core, blockSyntax)
|
|
881
|
+
val selStart = selectionStart.coerceIn(0, editable.length)
|
|
882
|
+
val selEnd = selectionEnd.coerceIn(selStart, editable.length)
|
|
883
|
+
|
|
884
|
+
val plainText = prefix + parsed.plainText + suffix
|
|
885
|
+
val contentStart = selStart + prefix.length
|
|
886
|
+
|
|
887
|
+
replaceTextInRange(selStart, selEnd, plainText) { editable ->
|
|
888
|
+
for (range in parsed.formattingRanges) {
|
|
889
|
+
formattingStore.addRange(
|
|
890
|
+
FormattingRange(range.type, range.start + contentStart, range.end + contentStart, range.url),
|
|
891
|
+
)
|
|
892
|
+
}
|
|
893
|
+
for (block in parsed.blockRanges) {
|
|
894
|
+
blockStore.setBlock(block.type, block.level, block.start + contentStart, block.end + contentStart, editable)
|
|
895
|
+
}
|
|
896
|
+
applyMathAtoms(editable, parsed.mathAtoms, offset = contentStart)
|
|
897
|
+
setSelection(selStart + plainText.length)
|
|
898
|
+
detectorPipeline.processTextChange(editable, editable.toString(), selStart, plainText.length)
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/** Toggles a heading (H1-H6) on the cursor's paragraph(s); the active level toggles back to a paragraph. */
|
|
903
|
+
fun toggleHeading(level: Int) {
|
|
904
|
+
val blockType = BlockType.forHeadingLevel(level) ?: return
|
|
905
|
+
toggleBlockType(blockType, level)
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
private fun toggleBlockType(
|
|
909
|
+
type: BlockType,
|
|
910
|
+
level: Int,
|
|
911
|
+
) {
|
|
912
|
+
val editable = text ?: return
|
|
913
|
+
val selStart = selectionStart.coerceIn(0, editable.length)
|
|
914
|
+
val selEnd = selectionEnd.coerceIn(selStart, editable.length)
|
|
915
|
+
blockCoordinator.toggleBlock(editable, type, level, selStart, selEnd)
|
|
916
|
+
applyFormattingAndEmit()
|
|
917
|
+
syncCursorSizeWithBlock()
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
private fun blockOnParagraphAt(pos: Int): BlockRange? {
|
|
921
|
+
val editable = text ?: return null
|
|
922
|
+
return blockCoordinator.blockAtPosition(editable, pos)
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
private fun listBlockStartingAt(lineStart: Int): BlockRange? = blockCoordinator.listBlockAtLineStart(lineStart)
|
|
926
|
+
|
|
927
|
+
fun headingLevelAtCursor(): Int {
|
|
928
|
+
val editable = text ?: return 0
|
|
929
|
+
return blockCoordinator.headingLevelAtPosition(editable, selectionStart)
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* On empty text with a heading block, overrides text size to the heading's
|
|
934
|
+
* font size so the cursor matches heading height. Hides the hint while
|
|
935
|
+
* active. Cleared automatically when text is typed or heading is toggled off.
|
|
936
|
+
*/
|
|
937
|
+
private fun syncCursorSizeWithBlock() {
|
|
938
|
+
val editable = text ?: return
|
|
939
|
+
val block = blockOnParagraphAt(selectionStart)
|
|
940
|
+
|
|
941
|
+
if (block != null && block.type in BlockType.HEADINGS && editable.isEmpty()) {
|
|
942
|
+
val headingSizePx = formatter.resolveHeadingFontSizePx(block.level) ?: return
|
|
943
|
+
if (headingOverrideBaseSizePx == null) {
|
|
944
|
+
headingOverrideBaseSizePx = paint.textSize
|
|
945
|
+
baseHintColor = currentHintTextColor
|
|
946
|
+
setHintTextColor(Color.TRANSPARENT)
|
|
947
|
+
}
|
|
948
|
+
if (paint.textSize != headingSizePx) {
|
|
949
|
+
setTextSize(TypedValue.COMPLEX_UNIT_PX, headingSizePx)
|
|
950
|
+
}
|
|
951
|
+
} else {
|
|
952
|
+
headingOverrideBaseSizePx?.let { baseSizePx ->
|
|
953
|
+
setTextSize(TypedValue.COMPLEX_UNIT_PX, baseSizePx)
|
|
954
|
+
headingOverrideBaseSizePx = null
|
|
955
|
+
baseHintColor?.let { setHintTextColor(it) }
|
|
956
|
+
baseHintColor = null
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
private val receiveContentListener =
|
|
962
|
+
OnReceiveContentListener { _, payload ->
|
|
963
|
+
// The listener also sees autofill, drag-and-drop, IME commitContent and
|
|
964
|
+
// PROCESS_TEXT. Without this filter an autofill would turn into a paste
|
|
965
|
+
// event — and a consumer would turn that into an attachment chip.
|
|
966
|
+
if (!interceptPaste || payload.source != ContentInfoCompat.SOURCE_CLIPBOARD) {
|
|
967
|
+
return@OnReceiveContentListener payload
|
|
968
|
+
}
|
|
969
|
+
val parts = payload.partition { it.text != null }
|
|
970
|
+
parts.first?.let { accepted ->
|
|
971
|
+
val clip = accepted.clip
|
|
972
|
+
val text = PasteTextUtils.joinClipTexts((0 until clip.itemCount).map { clip.getItemAt(it).text })
|
|
973
|
+
if (text.isNotEmpty()) {
|
|
974
|
+
eventEmitter.emitPaste(text, "")
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
// Hand back only what we did not claim, so the buffer stays untouched.
|
|
978
|
+
parts.second
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Keyboard clipboard chips (Gboard, Samsung Keyboard) paste through
|
|
983
|
+
* `InputConnection.commitText`, a route neither [onTextContextMenuItem] nor
|
|
984
|
+
* the receive-content listener sees, so the text landed in the buffer with no
|
|
985
|
+
* `onPaste`. A commit that equals the primary clip is treated as that paste
|
|
986
|
+
* and reported like the other two routes; the IME commit is then dropped and
|
|
987
|
+
* JS completes it through [resolvePaste].
|
|
988
|
+
*
|
|
989
|
+
* Short typing-sized commits are not compared, so the clipboard is not read
|
|
990
|
+
* (and Android 12's clipboard toast not shown) while the user types; see
|
|
991
|
+
* [PasteTextUtils.mayBeImePaste] and [PasteTextUtils.isImePaste].
|
|
992
|
+
*/
|
|
993
|
+
internal fun interceptImePaste(committed: String): Boolean {
|
|
994
|
+
if (!interceptPaste || !PasteTextUtils.mayBeImePaste(committed)) return false
|
|
995
|
+
val clip = readClipboardText()
|
|
996
|
+
if (!PasteTextUtils.isImePaste(committed, clip)) return false
|
|
997
|
+
eventEmitter.emitPaste(clip, "")
|
|
998
|
+
return true
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
private fun readClipboardText(): String {
|
|
1002
|
+
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager ?: return ""
|
|
1003
|
+
val clip = clipboard.primaryClip ?: return ""
|
|
1004
|
+
// `text`, not `coerceToText`: the listener can only claim items that already
|
|
1005
|
+
// carry text, and the two routes have to agree on what counts as a paste.
|
|
1006
|
+
return PasteTextUtils.joinClipTexts((0 until clip.itemCount).map { clip.getItemAt(it).text })
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Completes a paste reported through `onPaste`. `pastedText` lands verbatim —
|
|
1011
|
+
* not parsed as markdown, unlike the un-intercepted paste — and `markdown` is
|
|
1012
|
+
* ignored. Android reports `markdown` as `''` (see `onTextContextMenuItem`),
|
|
1013
|
+
* which is the contract 0.7.4 shipped with, although copy/cut here now tag the
|
|
1014
|
+
* clip with markdown through [MarkdownClipboard].
|
|
1015
|
+
*
|
|
1016
|
+
* Goes through [replaceTextInRange] like [pasteMarkdown], so the formatting and
|
|
1017
|
+
* block stores and the empty-list anchors are adjusted as for any other edit.
|
|
1018
|
+
*
|
|
1019
|
+
* Resolving with an empty string and an empty selection is a zero-length edit
|
|
1020
|
+
* — it still emits, which keeps the JS contract identical on both platforms.
|
|
1021
|
+
*/
|
|
1022
|
+
fun resolvePaste(
|
|
1023
|
+
pastedText: String,
|
|
1024
|
+
@Suppress("UNUSED_PARAMETER") markdown: String,
|
|
1025
|
+
) {
|
|
1026
|
+
val editable = text ?: return
|
|
1027
|
+
val selStart = selectionStart.coerceIn(0, editable.length)
|
|
1028
|
+
val selEnd = selectionEnd.coerceIn(selStart, editable.length)
|
|
1029
|
+
|
|
1030
|
+
replaceTextInRange(selStart, selEnd, pastedText) { ed ->
|
|
1031
|
+
setSelection(selStart + pastedText.length)
|
|
1032
|
+
// Parity with the un-intercepted paste: a pasted URL still autolinks.
|
|
1033
|
+
detectorPipeline.processTextChange(ed, ed.toString(), selStart, pastedText.length)
|
|
1034
|
+
}
|
|
1035
|
+
// The session suppresses onSelectionChanged while the edit runs, so the moved
|
|
1036
|
+
// caret is reported here.
|
|
1037
|
+
eventEmitter.emitSelection(selectionStart, selectionEnd)
|
|
1038
|
+
eventEmitter.emitCaretRectChangeIfNeeded()
|
|
489
1039
|
}
|
|
490
1040
|
|
|
491
1041
|
fun setLinkForSelection(url: String) {
|
|
492
1042
|
val selStart = selectionStart
|
|
493
1043
|
val selEnd = selectionEnd
|
|
494
1044
|
if (selStart == selEnd) return
|
|
495
|
-
|
|
496
|
-
val editable = text
|
|
497
|
-
if (editable != null) {
|
|
498
|
-
autoLinkDetector.clearAutoLinkInRange(editable, selStart, selEnd)
|
|
499
|
-
}
|
|
500
|
-
formattingStore.addRange(FormattingRange(StyleType.LINK, selStart, selEnd, url))
|
|
1045
|
+
linkCoordinator.setLinkForRange(url, selStart, selEnd, text)
|
|
501
1046
|
applyFormattingAndEmit()
|
|
502
1047
|
}
|
|
503
1048
|
|
|
1049
|
+
/** Parses the given markdown and inserts it at the cursor, replacing any selection. */
|
|
1050
|
+
fun insertTextAtCursor(markdown: String) {
|
|
1051
|
+
if (markdown.isEmpty()) return
|
|
1052
|
+
pasteMarkdown(markdown)
|
|
1053
|
+
}
|
|
1054
|
+
|
|
504
1055
|
fun insertLinkAtCursor(
|
|
505
1056
|
displayText: String,
|
|
506
1057
|
url: String,
|
|
507
1058
|
) {
|
|
508
|
-
val editable = text ?: return
|
|
509
1059
|
val selStart = selectionStart
|
|
510
1060
|
val selEnd = selectionEnd
|
|
511
1061
|
val linkEnd = selStart + displayText.length
|
|
512
1062
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
editable.replace(selStart, selEnd, displayText)
|
|
516
|
-
formattingStore.adjustForEdit(selStart, selEnd - selStart, displayText.length)
|
|
517
|
-
autoLinkDetector.clearAutoLinkInRange(editable, selStart, linkEnd)
|
|
518
|
-
formattingStore.addRange(FormattingRange(StyleType.LINK, selStart, linkEnd, sanitizeLinkUrl(url)))
|
|
519
|
-
lastProcessedText = editable.toString()
|
|
520
|
-
|
|
1063
|
+
replaceTextInRange(selStart, selEnd, displayText) { editable ->
|
|
1064
|
+
linkCoordinator.addLink(url, selStart, linkEnd, editable)
|
|
521
1065
|
setSelection(linkEnd)
|
|
522
|
-
applyFormattingAndEmit()
|
|
523
|
-
eventEmitter.emitChangeText()
|
|
524
|
-
} finally {
|
|
525
|
-
isProcessingTextChange = false
|
|
526
1066
|
}
|
|
527
1067
|
}
|
|
528
1068
|
|
|
@@ -531,207 +1071,116 @@ class EnrichedMarkdownTextInputView(
|
|
|
531
1071
|
url: String,
|
|
532
1072
|
) {
|
|
533
1073
|
if (displayText.isEmpty()) return
|
|
534
|
-
val indicator =
|
|
535
|
-
val start =
|
|
536
|
-
val end =
|
|
1074
|
+
val indicator = mentionCoordinator.currentIndicator ?: return
|
|
1075
|
+
val start = mentionCoordinator.currentStart
|
|
1076
|
+
val end = mentionCoordinator.currentEnd
|
|
537
1077
|
val editable = text ?: return
|
|
538
1078
|
if (start < 0 || end < start || end > editable.length) return
|
|
539
1079
|
|
|
540
|
-
val sanitizedUrl = sanitizeLinkUrl(url)
|
|
541
1080
|
val shouldAppendSpace = end >= editable.length || !editable[end].isWhitespace()
|
|
542
1081
|
val replacement = if (shouldAppendSpace) "$displayText " else displayText
|
|
543
1082
|
val linkEnd = start + displayText.length
|
|
544
1083
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
formattingStore.adjustForEdit(start, end - start, replacement.length)
|
|
549
|
-
autoLinkDetector.clearAutoLinkInRange(editable, start, linkEnd)
|
|
550
|
-
formattingStore.addRange(FormattingRange(StyleType.LINK, start, linkEnd, sanitizedUrl))
|
|
551
|
-
lastProcessedText = editable.toString()
|
|
552
|
-
|
|
553
|
-
clearActiveMention(emit = true, indicatorOverride = indicator)
|
|
1084
|
+
replaceTextInRange(start, end, replacement) { ed ->
|
|
1085
|
+
linkCoordinator.addLink(url, start, linkEnd, ed)
|
|
1086
|
+
dispatchMentionEvents(mentionCoordinator.clear(indicatorOverride = indicator))
|
|
554
1087
|
setSelection(start + replacement.length)
|
|
555
|
-
applyFormattingAndEmit()
|
|
556
|
-
eventEmitter.emitChangeText()
|
|
557
|
-
} finally {
|
|
558
|
-
isProcessingTextChange = false
|
|
559
1088
|
}
|
|
560
1089
|
}
|
|
561
1090
|
|
|
562
1091
|
fun startMention(indicator: String) {
|
|
563
|
-
if (indicator.isEmpty() || indicator
|
|
564
|
-
val editable = text ?: return
|
|
1092
|
+
if (indicator.isEmpty() || !mentionCoordinator.containsIndicator(indicator)) return
|
|
565
1093
|
val selStart = selectionStart
|
|
566
1094
|
val selEnd = selectionEnd
|
|
567
1095
|
|
|
568
|
-
|
|
569
|
-
try {
|
|
570
|
-
editable.replace(selStart, selEnd, indicator)
|
|
571
|
-
formattingStore.adjustForEdit(selStart, selEnd - selStart, indicator.length)
|
|
572
|
-
lastProcessedText = editable.toString()
|
|
1096
|
+
replaceTextInRange(selStart, selEnd, indicator) {
|
|
573
1097
|
setSelection(selStart + indicator.length)
|
|
574
|
-
applyFormattingAndEmit()
|
|
575
|
-
eventEmitter.emitChangeText()
|
|
576
|
-
updateActiveMention()
|
|
577
|
-
} finally {
|
|
578
|
-
isProcessingTextChange = false
|
|
579
1098
|
}
|
|
1099
|
+
dispatchMentionUpdate()
|
|
580
1100
|
}
|
|
581
1101
|
|
|
582
1102
|
fun removeLinkAtCursor() {
|
|
583
|
-
|
|
584
|
-
val linkRange = formattingStore.rangeOfType(StyleType.LINK, pos) ?: return
|
|
585
|
-
formattingStore.removeRange(linkRange)
|
|
1103
|
+
if (!linkCoordinator.removeLink(selectionStart)) return
|
|
586
1104
|
applyFormattingAndEmit()
|
|
587
1105
|
}
|
|
588
1106
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
applyFormattingAndEmit()
|
|
604
|
-
eventEmitter.emitChangeText()
|
|
605
|
-
clearActiveMention()
|
|
606
|
-
} finally {
|
|
607
|
-
isProcessingTextChange = false
|
|
608
|
-
}
|
|
609
|
-
return true
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
fun setMentionIndicators(indicators: List<String>) {
|
|
613
|
-
val newIndicators = LinkedHashSet(indicators)
|
|
614
|
-
if (newIndicators == mentionIndicators) return
|
|
615
|
-
mentionIndicators = newIndicators
|
|
616
|
-
activeMentionIndicator?.let { indicator ->
|
|
617
|
-
if (indicator !in mentionIndicators) {
|
|
618
|
-
clearActiveMention(emit = true, indicatorOverride = indicator)
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
updateActiveMention()
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
fun dismissActiveMention() {
|
|
625
|
-
clearActiveMention(emit = false)
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
fun setContextMenuItems(items: List<String>) {
|
|
629
|
-
contextMenu.setContextMenuItems(items)
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
fun setLinkRegex(config: LinkRegexConfig) {
|
|
633
|
-
autoLinkDetector.setRegexConfig(config)
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
fun setAutoLinkStyle(style: InputFormatterStyle) {
|
|
637
|
-
autoLinkDetector.style = style
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
fun allFormattingRangesForSerialization(): List<FormattingRange> {
|
|
641
|
-
val editable = text ?: return formattingStore.allRanges
|
|
642
|
-
val transientRanges = detectorPipeline.allTransientFormattingRanges(editable)
|
|
643
|
-
if (transientRanges.isEmpty()) return formattingStore.allRanges
|
|
644
|
-
return formattingStore.allRanges + transientRanges
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
fun setValueFromJS(markdown: String) {
|
|
648
|
-
lastMarkdownFromJS = markdown
|
|
649
|
-
val parsed = InputParser.parseToPlainTextAndRanges(markdown, blockSyntax)
|
|
650
|
-
blockEmitting = true
|
|
651
|
-
try {
|
|
652
|
-
runAsATransaction {
|
|
653
|
-
formattingStore.clearAll()
|
|
654
|
-
formattingStore.setRanges(parsed.formattingRanges)
|
|
655
|
-
setText(parsed.plainText)
|
|
656
|
-
val editable = text
|
|
657
|
-
if (editable != null && parsed.mathAtoms.isNotEmpty()) {
|
|
658
|
-
applyMathAtoms(editable, parsed.mathAtoms)
|
|
659
|
-
}
|
|
660
|
-
setSelection(text?.length ?: 0)
|
|
661
|
-
}
|
|
662
|
-
applyFormatting()
|
|
663
|
-
forceScrollToSelection()
|
|
664
|
-
layoutManager.invalidateLayout()
|
|
665
|
-
lastProcessedText = text?.toString() ?: ""
|
|
666
|
-
} finally {
|
|
667
|
-
blockEmitting = false
|
|
668
|
-
}
|
|
1107
|
+
// region Math chip
|
|
1108
|
+
//
|
|
1109
|
+
// A chip is one OBJECT_REPLACEMENT character in the buffer plus a MathChipSpan that
|
|
1110
|
+
// carries the LaTeX and draws the rendered formula. Nothing about the formula lives
|
|
1111
|
+
// in the text, so the caret, the IME and every range in the stores treat it as a
|
|
1112
|
+
// single atomic character. All text mutation goes through the same
|
|
1113
|
+
// replaceTextInRange/EditPhase machinery as links and mentions — the pre-merge fork
|
|
1114
|
+
// hand-rolled its own re-entrancy flags, which no longer exist.
|
|
1115
|
+
|
|
1116
|
+
/** Chips in document order, as plain data for [MarkdownSerializer]. Empty with math off. */
|
|
1117
|
+
fun mathChipsForSerialization(): List<MathChipController.ChipInfo> {
|
|
1118
|
+
val controller = mathChipController ?: return emptyList()
|
|
1119
|
+
val editable = text ?: return emptyList()
|
|
1120
|
+
return controller.allChips(editable)
|
|
669
1121
|
}
|
|
670
1122
|
|
|
1123
|
+
/**
|
|
1124
|
+
* Stamps parsed [atoms] onto placeholder characters already present in [editable],
|
|
1125
|
+
* shifting their positions by [offset] (non-zero when the parse covered an inserted
|
|
1126
|
+
* fragment rather than the whole buffer).
|
|
1127
|
+
*/
|
|
671
1128
|
private fun applyMathAtoms(
|
|
672
1129
|
editable: Editable,
|
|
673
|
-
atoms: List<
|
|
1130
|
+
atoms: List<MathAtom>,
|
|
1131
|
+
offset: Int = 0,
|
|
674
1132
|
) {
|
|
675
1133
|
val controller = mathChipController ?: return
|
|
1134
|
+
// Clear first: a math-free import still has to disarm atoms deferred by an earlier
|
|
1135
|
+
// buffer, or they would be stamped onto whatever text replaced them.
|
|
1136
|
+
pendingMathAtoms = emptyList()
|
|
1137
|
+
if (atoms.isEmpty()) return
|
|
1138
|
+
val positioned = if (offset == 0) atoms else atoms.map { it.copy(position = it.position + offset) }
|
|
676
1139
|
val style = inputStyle
|
|
677
1140
|
if (style == null) {
|
|
678
|
-
// `markdownStyle`
|
|
679
|
-
//
|
|
680
|
-
|
|
1141
|
+
// `markdownStyle` has not arrived yet. @ReactProp setters fire in undefined order,
|
|
1142
|
+
// so `defaultValue` can be applied first — the only producer that reaches this
|
|
1143
|
+
// branch, which is why a plain assignment (not an append) is correct here.
|
|
1144
|
+
pendingMathAtoms = positioned
|
|
681
1145
|
return
|
|
682
1146
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
1147
|
+
controller.applyAtoms(editable, positioned, style.inlineMath, style.math)
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Pushes a new `markdownStyle` into every live chip, and drains any atoms that were
|
|
1152
|
+
* parsed before the style existed. Both cases move the measured text height, which
|
|
1153
|
+
* Fabric owns — [InputLayoutManager.invalidateLayout] is the only way to change it.
|
|
1154
|
+
*/
|
|
1155
|
+
private fun syncMathChipsWithStyle(style: InputFormatterStyle) {
|
|
1156
|
+
val controller = mathChipController ?: return
|
|
1157
|
+
val editable = text ?: return
|
|
1158
|
+
val pending = pendingMathAtoms
|
|
1159
|
+
if (pending.isNotEmpty()) {
|
|
1160
|
+
pendingMathAtoms = emptyList()
|
|
1161
|
+
controller.applyAtoms(editable, pending, style.inlineMath, style.math)
|
|
1162
|
+
} else {
|
|
1163
|
+
// Chips cache the style object they were built with, so a style pass has to
|
|
1164
|
+
// re-stamp them; with no chip in the buffer there is nothing to do and no
|
|
1165
|
+
// reason to make Fabric re-measure.
|
|
1166
|
+
if (controller.allChips(editable).isEmpty()) return
|
|
1167
|
+
controller.refreshAll(editable, style.inlineMath, style.math)
|
|
689
1168
|
}
|
|
1169
|
+
invalidate()
|
|
1170
|
+
requestLayout()
|
|
1171
|
+
layoutManager.invalidateLayout()
|
|
690
1172
|
}
|
|
691
1173
|
|
|
1174
|
+
/** Re-stamps every chip with the current style — used when the row width changes. */
|
|
692
1175
|
fun refreshAllMathChips() {
|
|
693
1176
|
val controller = mathChipController ?: return
|
|
694
1177
|
val editable = text ?: return
|
|
695
1178
|
val style = inputStyle ?: return
|
|
696
|
-
controller.
|
|
1179
|
+
if (controller.allChips(editable).isEmpty()) return
|
|
1180
|
+
controller.refreshAll(editable, style.inlineMath, style.math)
|
|
697
1181
|
invalidate()
|
|
698
1182
|
requestLayout()
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
fun applyInputStyle(style: InputFormatterStyle): Boolean {
|
|
702
|
-
val previous = inputStyle
|
|
703
|
-
inputStyle = style
|
|
704
|
-
val changed = previous != style
|
|
705
|
-
val mathStyleChanged =
|
|
706
|
-
previous == null ||
|
|
707
|
-
previous.math != style.math ||
|
|
708
|
-
previous.mathBlock != style.mathBlock
|
|
709
|
-
|
|
710
|
-
val controller = mathChipController
|
|
711
|
-
val editable = text
|
|
712
|
-
if (controller != null && editable != null && pendingMathAtoms.isNotEmpty()) {
|
|
713
|
-
val atoms = pendingMathAtoms
|
|
714
|
-
pendingMathAtoms = emptyList()
|
|
715
|
-
isProcessingTextChange = true
|
|
716
|
-
try {
|
|
717
|
-
controller.applyAtoms(editable, atoms, style.math, style.mathBlock)
|
|
718
|
-
} finally {
|
|
719
|
-
isProcessingTextChange = false
|
|
720
|
-
}
|
|
721
|
-
invalidate()
|
|
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()
|
|
727
|
-
} else if (mathStyleChanged) {
|
|
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()
|
|
733
|
-
}
|
|
734
|
-
return changed
|
|
1183
|
+
layoutManager.invalidateLayout()
|
|
735
1184
|
}
|
|
736
1185
|
|
|
737
1186
|
fun insertMath(
|
|
@@ -743,42 +1192,32 @@ class EnrichedMarkdownTextInputView(
|
|
|
743
1192
|
val style = inputStyle ?: return
|
|
744
1193
|
val editable = text ?: return
|
|
745
1194
|
|
|
746
|
-
val selStart = selectionStart.
|
|
747
|
-
val selEnd = selectionEnd.
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
val
|
|
753
|
-
if (displayMode)
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
1195
|
+
val selStart = selectionStart.coerceIn(0, editable.length)
|
|
1196
|
+
val selEnd = selectionEnd.coerceIn(selStart, editable.length)
|
|
1197
|
+
|
|
1198
|
+
// A block chip claims the full row, so it needs a line of its own: add the
|
|
1199
|
+
// separators the caret position is missing rather than pulling the surrounding
|
|
1200
|
+
// text into the chip's line.
|
|
1201
|
+
val prefix = if (displayMode && selStart > 0 && !editable[selStart - 1].isLineBreak()) "\n" else ""
|
|
1202
|
+
val suffix = if (displayMode && selEnd < editable.length && !editable[selEnd].isLineBreak()) "\n" else ""
|
|
1203
|
+
val replacement = prefix + OBJECT_REPLACEMENT + suffix
|
|
1204
|
+
val chipPosition = selStart + prefix.length
|
|
1205
|
+
// style.inlineMath is `markdownStyle.inlineMath`, style.math is the block key —
|
|
1206
|
+
// they swapped names in the P7 rename and both are chip styles, so a stale read
|
|
1207
|
+
// compiles clean and styles the wrong chip.
|
|
1208
|
+
val chipStyle: MathChipStyle = if (displayMode) style.math else style.inlineMath
|
|
1209
|
+
|
|
1210
|
+
replaceTextInRange(selStart, selEnd, replacement) { updated ->
|
|
1211
|
+
controller.insertChip(updated, chipPosition, latex, displayMode, chipStyle)
|
|
1212
|
+
setSelection((selStart + replacement.length).coerceAtMost(updated.length))
|
|
758
1213
|
}
|
|
759
|
-
val replacement = "$prefix$suffix"
|
|
760
|
-
val chipPositionInReplacement = prefix.length
|
|
761
1214
|
|
|
762
|
-
isProcessingTextChange = true
|
|
763
|
-
try {
|
|
764
|
-
editable.replace(safeStart, safeEnd, replacement)
|
|
765
|
-
val chipPosition = safeStart + chipPositionInReplacement
|
|
766
|
-
val chipStyle: MathChipStyle = if (displayMode) style.mathBlock else style.math
|
|
767
|
-
controller.insertChip(editable, chipPosition, latex, displayMode, chipStyle)
|
|
768
|
-
formattingStore.adjustForEdit(safeStart, safeEnd - safeStart, replacement.length)
|
|
769
|
-
lastProcessedText = editable.toString()
|
|
770
|
-
setSelection(safeStart + replacement.length)
|
|
771
|
-
} finally {
|
|
772
|
-
isProcessingTextChange = false
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
applyFormatting()
|
|
776
1215
|
invalidate()
|
|
777
1216
|
requestLayout()
|
|
778
|
-
|
|
779
|
-
|
|
1217
|
+
layoutManager.invalidateLayout()
|
|
1218
|
+
// replaceTextInRange emits text/markdown/state; the selection it set was applied
|
|
1219
|
+
// inside the Processing phase, where onSelectionChanged bails out.
|
|
780
1220
|
eventEmitter.emitSelection(selectionStart, selectionEnd)
|
|
781
|
-
eventEmitter.emitState()
|
|
782
1221
|
eventEmitter.emitCaretRectChangeIfNeeded()
|
|
783
1222
|
}
|
|
784
1223
|
|
|
@@ -790,11 +1229,13 @@ class EnrichedMarkdownTextInputView(
|
|
|
790
1229
|
val controller = mathChipController ?: return
|
|
791
1230
|
val style = inputStyle ?: return
|
|
792
1231
|
val editable = text ?: return
|
|
793
|
-
val chipStyle: MathChipStyle = if (displayMode) style.
|
|
794
|
-
|
|
795
|
-
|
|
1232
|
+
val chipStyle: MathChipStyle = if (displayMode) style.math else style.inlineMath
|
|
1233
|
+
// Formula and mode live on the span, so this changes no text: no store adjustment
|
|
1234
|
+
// and no EditPhase are needed, only a re-render and a re-measure.
|
|
1235
|
+
if (!controller.updateChip(editable, start, latex, displayMode, chipStyle).applied) return
|
|
796
1236
|
invalidate()
|
|
797
1237
|
requestLayout()
|
|
1238
|
+
layoutManager.invalidateLayout()
|
|
798
1239
|
if (emitMarkdown) eventEmitter.emitChangeMarkdown()
|
|
799
1240
|
}
|
|
800
1241
|
|
|
@@ -802,22 +1243,32 @@ class EnrichedMarkdownTextInputView(
|
|
|
802
1243
|
val controller = mathChipController ?: return
|
|
803
1244
|
val editable = text ?: return
|
|
804
1245
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1246
|
+
// The controller does the delete because it also drops the chip's AlignmentSpan,
|
|
1247
|
+
// which is SPAN_PARAGRAPH and would otherwise outlive the character and leave the
|
|
1248
|
+
// rest of the paragraph centered.
|
|
1249
|
+
var removed = false
|
|
1250
|
+
editSession.enter(EditPhase.Processing)
|
|
1251
|
+
try {
|
|
1252
|
+
removed = controller.removeChip(editable, start)
|
|
1253
|
+
if (removed) {
|
|
1254
|
+
adjustStoresForEdit(start, 1, 0)
|
|
1255
|
+
lastProcessedText = editable.toString()
|
|
1256
|
+
applyFormattingAndEmit()
|
|
1257
|
+
eventEmitter.emitChangeText()
|
|
811
1258
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1259
|
+
} finally {
|
|
1260
|
+
editSession.exit()
|
|
1261
|
+
}
|
|
1262
|
+
if (!removed) return
|
|
1263
|
+
if (activeMathChipPosition == start) activeMathChipPosition = -1
|
|
1264
|
+
invalidate()
|
|
1265
|
+
requestLayout()
|
|
1266
|
+
layoutManager.invalidateLayout()
|
|
818
1267
|
eventEmitter.emitSelection(selectionStart, selectionEnd)
|
|
1268
|
+
eventEmitter.emitCaretRectChangeIfNeeded()
|
|
819
1269
|
}
|
|
820
1270
|
|
|
1271
|
+
/** Drops the selected styling from every chip except [except] (all of them when -1). */
|
|
821
1272
|
private fun clearMathChipSelections(except: Int = -1) {
|
|
822
1273
|
val controller = mathChipController ?: return
|
|
823
1274
|
val editable = text ?: return
|
|
@@ -827,10 +1278,8 @@ class EnrichedMarkdownTextInputView(
|
|
|
827
1278
|
} else {
|
|
828
1279
|
var didChange = false
|
|
829
1280
|
for (chip in controller.allChips(editable)) {
|
|
830
|
-
if (chip.position == except) continue
|
|
831
|
-
if (chip.
|
|
832
|
-
if (controller.setSelected(editable, chip.position, false)) didChange = true
|
|
833
|
-
}
|
|
1281
|
+
if (chip.position == except || !chip.isSelected) continue
|
|
1282
|
+
if (controller.setSelected(editable, chip.position, false)) didChange = true
|
|
834
1283
|
}
|
|
835
1284
|
didChange
|
|
836
1285
|
}
|
|
@@ -841,19 +1290,20 @@ class EnrichedMarkdownTextInputView(
|
|
|
841
1290
|
val controller = mathChipController ?: return false
|
|
842
1291
|
val editable = text ?: return false
|
|
843
1292
|
val info = controller.chipAt(editable, chipPosition) ?: return false
|
|
844
|
-
val
|
|
1293
|
+
val textLayout = layout ?: return false
|
|
845
1294
|
|
|
846
1295
|
clearMathChipSelections(except = info.position)
|
|
847
1296
|
controller.setSelected(editable, info.position, true)
|
|
848
1297
|
invalidate()
|
|
1298
|
+
// Set before setSelection: onSelectionChanged reads it to decide whether the
|
|
1299
|
+
// caret is still on the chip, and would otherwise clear the highlight at once.
|
|
849
1300
|
activeMathChipPosition = info.position
|
|
850
|
-
// Caret
|
|
851
|
-
//
|
|
852
|
-
//
|
|
853
|
-
// selected style alive.
|
|
1301
|
+
// Caret after the chip, not a length-1 selection: a selection would draw the
|
|
1302
|
+
// system's selection handles over the chip glyph and trip the `selStart == selEnd`
|
|
1303
|
+
// guard that keeps the chip's selected styling alive.
|
|
854
1304
|
setSelection((info.position + 1).coerceAtMost(editable.length))
|
|
855
1305
|
|
|
856
|
-
val bounds = computeChipBounds(
|
|
1306
|
+
val bounds = computeChipBounds(textLayout, info.position)
|
|
857
1307
|
val density = resources.displayMetrics.density.toDouble()
|
|
858
1308
|
eventEmitter.emitMathPress(
|
|
859
1309
|
info.latex,
|
|
@@ -870,57 +1320,58 @@ class EnrichedMarkdownTextInputView(
|
|
|
870
1320
|
|
|
871
1321
|
/** Chip glyph rect in [Layout] coordinates — no padding, no scroll offset. */
|
|
872
1322
|
private fun chipBoundsInLayout(
|
|
873
|
-
|
|
1323
|
+
textLayout: Layout,
|
|
874
1324
|
start: Int,
|
|
875
1325
|
): Rect {
|
|
876
|
-
val line =
|
|
877
|
-
val xStart =
|
|
878
|
-
val xEnd =
|
|
879
|
-
// Ordered, not raw start/end: in an RTL run xStart > xEnd, which would
|
|
880
|
-
// inverted Rect
|
|
1326
|
+
val line = textLayout.getLineForOffset(start)
|
|
1327
|
+
val xStart = textLayout.getPrimaryHorizontal(start).toInt()
|
|
1328
|
+
val xEnd = textLayout.getPrimaryHorizontal(start + 1).toInt()
|
|
1329
|
+
// Ordered, not raw start/end: in an RTL run xStart > xEnd, which would build an
|
|
1330
|
+
// inverted Rect whose contains() always rejects.
|
|
881
1331
|
val left = xStart.coerceAtMost(xEnd)
|
|
882
1332
|
var right = xStart.coerceAtLeast(xEnd)
|
|
883
1333
|
if (right <= left) {
|
|
884
1334
|
// 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
|
|
886
|
-
// so the rect keeps a real width instead of collapsing.
|
|
887
|
-
right =
|
|
1335
|
+
// report the same horizontal for both offsets. Fall back to the line's content
|
|
1336
|
+
// edge so the rect keeps a real width instead of collapsing.
|
|
1337
|
+
right = textLayout.getLineRight(line).toInt()
|
|
888
1338
|
}
|
|
889
|
-
return Rect(left,
|
|
1339
|
+
return Rect(left, textLayout.getLineTop(line), right, textLayout.getLineBottom(line))
|
|
890
1340
|
}
|
|
891
1341
|
|
|
892
1342
|
/** Chip glyph rect in view coordinates, for reporting to JS. */
|
|
893
1343
|
private fun computeChipBounds(
|
|
894
|
-
|
|
1344
|
+
textLayout: Layout,
|
|
895
1345
|
start: Int,
|
|
896
1346
|
): Rect =
|
|
897
|
-
chipBoundsInLayout(
|
|
1347
|
+
chipBoundsInLayout(textLayout, start).apply {
|
|
898
1348
|
offset(totalPaddingLeft - scrollX, totalPaddingTop - scrollY)
|
|
899
1349
|
}
|
|
900
1350
|
|
|
1351
|
+
/** Offset of the chip under a touch point, or -1 when the touch missed every chip. */
|
|
901
1352
|
fun mathChipPositionAtTouch(
|
|
902
1353
|
x: Float,
|
|
903
1354
|
y: Float,
|
|
904
1355
|
): Int {
|
|
905
1356
|
val controller = mathChipController ?: return -1
|
|
906
1357
|
val editable = text ?: return -1
|
|
907
|
-
val
|
|
1358
|
+
val textLayout = layout ?: return -1
|
|
908
1359
|
val adjustedX = (x - totalPaddingLeft + scrollX).toInt()
|
|
909
1360
|
val adjustedY = (y - totalPaddingTop + scrollY).toInt()
|
|
910
|
-
// getLineForVertical and getOffsetForHorizontal both clamp into the laid-out text,
|
|
911
|
-
// a touch in the padding or in the blank space below the last line resolves onto
|
|
912
|
-
// nearest character. Bound the touch to the text box, then require it to
|
|
913
|
-
// the chip's own rect — otherwise a tap beside a chip reads as a tap on it.
|
|
914
|
-
if (adjustedY < 0 || adjustedY >
|
|
915
|
-
val line =
|
|
916
|
-
val offset =
|
|
1361
|
+
// getLineForVertical and getOffsetForHorizontal both clamp into the laid-out text,
|
|
1362
|
+
// so a touch in the padding or in the blank space below the last line resolves onto
|
|
1363
|
+
// the nearest character. Bound the touch to the text box first, then require it to
|
|
1364
|
+
// land inside the chip's own rect — otherwise a tap beside a chip reads as a tap on it.
|
|
1365
|
+
if (adjustedY < 0 || adjustedY > textLayout.height) return -1
|
|
1366
|
+
val line = textLayout.getLineForVertical(adjustedY)
|
|
1367
|
+
val offset = textLayout.getOffsetForHorizontal(line, adjustedX.toFloat())
|
|
917
1368
|
if (offset < 0) return -1
|
|
918
1369
|
val candidates = intArrayOf(offset, offset - 1).filter { it in 0 until editable.length }
|
|
919
1370
|
for (candidate in candidates) {
|
|
920
1371
|
val range = controller.chipRangeAt(editable, candidate) ?: continue
|
|
921
1372
|
// An empty rect means the layout could not give us the chip's extent; accept the
|
|
922
1373
|
// candidate then, rather than making the chip untappable.
|
|
923
|
-
val bounds = chipBoundsInLayout(
|
|
1374
|
+
val bounds = chipBoundsInLayout(textLayout, range.first)
|
|
924
1375
|
if (!bounds.isEmpty && !bounds.contains(adjustedX, adjustedY)) continue
|
|
925
1376
|
return range.first
|
|
926
1377
|
}
|
|
@@ -928,13 +1379,13 @@ class EnrichedMarkdownTextInputView(
|
|
|
928
1379
|
}
|
|
929
1380
|
|
|
930
1381
|
/**
|
|
931
|
-
*
|
|
932
|
-
*
|
|
933
|
-
*
|
|
1382
|
+
* Shrinks the IME composing region so it never overlaps a math chip, called from
|
|
1383
|
+
* [com.swmansion.enriched.markdown.input.editing.InputConnectionWrapper.setComposingRegion].
|
|
1384
|
+
* Returns the adjusted (start, end) pair, or null when there is no chip controller —
|
|
1385
|
+
* the caller then keeps the original range.
|
|
934
1386
|
*
|
|
935
|
-
*
|
|
936
|
-
*
|
|
937
|
-
* chip's `` placeholder character.
|
|
1387
|
+
* Where the region intersects a chip it collapses to the side furthest from the chip,
|
|
1388
|
+
* so a Hangul/Kana composing block cannot swallow the chip's placeholder character.
|
|
938
1389
|
*/
|
|
939
1390
|
fun adjustComposingRegionAwayFromChips(
|
|
940
1391
|
start: Int,
|
|
@@ -954,7 +1405,7 @@ class EnrichedMarkdownTextInputView(
|
|
|
954
1405
|
i++
|
|
955
1406
|
continue
|
|
956
1407
|
}
|
|
957
|
-
//
|
|
1408
|
+
// The region overlaps a chip — push out the edge nearest to it.
|
|
958
1409
|
val distFromStart = range.first - adjustedStart
|
|
959
1410
|
val distFromEnd = adjustedEnd - (range.last + 1)
|
|
960
1411
|
if (distFromStart >= distFromEnd) {
|
|
@@ -968,6 +1419,80 @@ class EnrichedMarkdownTextInputView(
|
|
|
968
1419
|
if (adjustedStart >= adjustedEnd) return adjustedStart to adjustedStart
|
|
969
1420
|
return adjustedStart to adjustedEnd
|
|
970
1421
|
}
|
|
1422
|
+
// endregion
|
|
1423
|
+
|
|
1424
|
+
fun deleteLinkBeforeCursor(): Boolean {
|
|
1425
|
+
val cursorStart = selectionStart
|
|
1426
|
+
val cursorEnd = selectionEnd
|
|
1427
|
+
if (cursorStart != cursorEnd || cursorStart <= 0) return false
|
|
1428
|
+
if (text == null) return false
|
|
1429
|
+
|
|
1430
|
+
val linkRange = linkCoordinator.linkRangeForDeletion(cursorStart) ?: return false
|
|
1431
|
+
|
|
1432
|
+
replaceTextInRange(linkRange.start, linkRange.end, "") { editable ->
|
|
1433
|
+
setSelection(linkRange.start.coerceAtMost(editable.length))
|
|
1434
|
+
}
|
|
1435
|
+
dispatchMentionEvents(mentionCoordinator.clear())
|
|
1436
|
+
return true
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
fun setMentionIndicators(indicators: List<String>) {
|
|
1440
|
+
dispatchMentionEvents(mentionCoordinator.setIndicators(indicators))
|
|
1441
|
+
dispatchMentionUpdate()
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
fun dismissActiveMention() {
|
|
1445
|
+
mentionCoordinator.clear(emit = false)
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
fun setContextMenuItems(items: List<String>) {
|
|
1449
|
+
contextMenu.setContextMenuItems(items)
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
fun setLinkRegex(config: LinkRegexConfig) {
|
|
1453
|
+
autoLinkDetector.setRegexConfig(config)
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
fun setAutoLinkStyle(style: InputFormatterStyle) {
|
|
1457
|
+
autoLinkDetector.style = style
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* Applies the parsed `markdownStyle` (which now carries `list.itemSpacing`).
|
|
1462
|
+
* Display density is folded in at [InputFormatter] construction, so the style is
|
|
1463
|
+
* handed to the formatter as-is. Returns true if the effective style changed
|
|
1464
|
+
* (caller re-applies formatting).
|
|
1465
|
+
*/
|
|
1466
|
+
fun setMarkdownStyleFromProps(style: InputFormatterStyle): Boolean {
|
|
1467
|
+
setAutoLinkStyle(style)
|
|
1468
|
+
inputStyle = style
|
|
1469
|
+
val changed = formatter.updateStyle(style)
|
|
1470
|
+
syncMathChipsWithStyle(style)
|
|
1471
|
+
return changed
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
fun allFormattingRangesForSerialization(): List<FormattingRange> = clipboardCoordinator.allRangesForSerialization(text)
|
|
1475
|
+
|
|
1476
|
+
fun setValueFromJS(markdown: String) {
|
|
1477
|
+
lastMarkdownFromJS = markdown
|
|
1478
|
+
val parsed = InputParser.parseToPlainTextAndRanges(markdown, blockSyntax)
|
|
1479
|
+
editSession.enter(EditPhase.Importing)
|
|
1480
|
+
try {
|
|
1481
|
+
formattingStore.clearAll()
|
|
1482
|
+
formattingStore.setRanges(parsed.formattingRanges)
|
|
1483
|
+
blockStore.setRanges(parsed.blockRanges)
|
|
1484
|
+
setText(parsed.plainText)
|
|
1485
|
+
text?.let { applyMathAtoms(it, parsed.mathAtoms) }
|
|
1486
|
+
setSelection(text?.length ?: 0)
|
|
1487
|
+
zwspAnchorCount = 0
|
|
1488
|
+
applyFormatting()
|
|
1489
|
+
forceScrollToSelection()
|
|
1490
|
+
layoutManager.invalidateLayout()
|
|
1491
|
+
lastProcessedText = text?.toString() ?: ""
|
|
1492
|
+
} finally {
|
|
1493
|
+
editSession.exit()
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
971
1496
|
|
|
972
1497
|
override fun setBackgroundColor(color: Int) {
|
|
973
1498
|
BackgroundStyleApplicator.setBackgroundColor(this, color)
|
|
@@ -1067,15 +1592,7 @@ class EnrichedMarkdownTextInputView(
|
|
|
1067
1592
|
) {
|
|
1068
1593
|
if (start >= end) return
|
|
1069
1594
|
val handler = formatter.handlers[styleType] ?: return
|
|
1070
|
-
|
|
1071
|
-
if (isActive) {
|
|
1072
|
-
formattingStore.removeType(styleType, start, end)
|
|
1073
|
-
} else {
|
|
1074
|
-
for (conflict in handler.mergingConfig.conflictingStyles) {
|
|
1075
|
-
formattingStore.removeType(conflict, start, end)
|
|
1076
|
-
}
|
|
1077
|
-
formattingStore.addRange(FormattingRange(styleType, start, end))
|
|
1078
|
-
}
|
|
1595
|
+
formattingStore.toggleStyle(styleType, start, end, handler.mergingConfig.conflictingStyles)
|
|
1079
1596
|
applyFormattingAndEmit()
|
|
1080
1597
|
}
|
|
1081
1598
|
|
|
@@ -1084,81 +1601,25 @@ class EnrichedMarkdownTextInputView(
|
|
|
1084
1601
|
start: Int,
|
|
1085
1602
|
end: Int,
|
|
1086
1603
|
) {
|
|
1087
|
-
|
|
1088
|
-
formattingStore.addRange(FormattingRange(StyleType.LINK, start, end, url))
|
|
1604
|
+
linkCoordinator.addLinkDirect(url, start, end)
|
|
1089
1605
|
applyFormattingAndEmit()
|
|
1090
1606
|
}
|
|
1091
1607
|
|
|
1092
|
-
private fun
|
|
1093
|
-
|
|
1094
|
-
val cursor = selectionStart
|
|
1095
|
-
if (selectionStart != selectionEnd || cursor < 0 || cursor > plainText.length) {
|
|
1096
|
-
clearActiveMention()
|
|
1097
|
-
return
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
val candidate = detectMentionAtCursor(plainText, cursor)
|
|
1101
|
-
if (candidate == null) {
|
|
1102
|
-
clearActiveMention()
|
|
1103
|
-
return
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
if (activeMentionIndicator != candidate.indicator || activeMentionStart != candidate.start) {
|
|
1107
|
-
activeMentionIndicator?.let { eventEmitter.emitEndMention(it) }
|
|
1108
|
-
activeMentionIndicator = candidate.indicator
|
|
1109
|
-
activeMentionStart = candidate.start
|
|
1110
|
-
eventEmitter.emitStartMention(candidate.indicator)
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
activeMentionEnd = candidate.end
|
|
1114
|
-
if (activeMentionText != candidate.text) {
|
|
1115
|
-
activeMentionText = candidate.text
|
|
1116
|
-
eventEmitter.emitChangeMention(candidate.indicator, candidate.text)
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
private fun detectMentionAtCursor(
|
|
1121
|
-
plainText: String,
|
|
1122
|
-
cursor: Int,
|
|
1123
|
-
): MentionCandidate? {
|
|
1124
|
-
if (mentionIndicators.isEmpty()) return null
|
|
1125
|
-
|
|
1126
|
-
val start = WordsUtils.tokenStart(plainText, cursor)
|
|
1127
|
-
val token = plainText.substring(start, cursor)
|
|
1128
|
-
val indicator = mentionIndicators.firstOrNull { token.startsWith(it) } ?: return null
|
|
1129
|
-
if (formattingStore.rangeOfType(StyleType.LINK, start) != null) return null
|
|
1130
|
-
|
|
1131
|
-
return MentionCandidate(
|
|
1132
|
-
indicator = indicator,
|
|
1133
|
-
start = start,
|
|
1134
|
-
end = cursor,
|
|
1135
|
-
text = token.substring(indicator.length),
|
|
1136
|
-
)
|
|
1608
|
+
private fun dispatchMentionUpdate() {
|
|
1609
|
+
dispatchMentionEvents(mentionCoordinator.update(text?.toString(), selectionStart, selectionEnd))
|
|
1137
1610
|
}
|
|
1138
1611
|
|
|
1139
|
-
private fun
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
activeMentionEnd = -1
|
|
1147
|
-
activeMentionText = ""
|
|
1148
|
-
if (emit && indicator != null) {
|
|
1149
|
-
eventEmitter.emitEndMention(indicator)
|
|
1612
|
+
private fun dispatchMentionEvents(events: List<MentionEvent>) {
|
|
1613
|
+
for (event in events) {
|
|
1614
|
+
when (event) {
|
|
1615
|
+
is MentionEvent.Start -> eventEmitter.emitStartMention(event.indicator)
|
|
1616
|
+
is MentionEvent.Change -> eventEmitter.emitChangeMention(event.indicator, event.text)
|
|
1617
|
+
is MentionEvent.End -> eventEmitter.emitEndMention(event.indicator)
|
|
1618
|
+
}
|
|
1150
1619
|
}
|
|
1151
1620
|
}
|
|
1152
1621
|
|
|
1153
|
-
private
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
.replace(")", "%29")
|
|
1157
|
-
|
|
1158
|
-
private data class MentionCandidate(
|
|
1159
|
-
val indicator: String,
|
|
1160
|
-
val start: Int,
|
|
1161
|
-
val end: Int,
|
|
1162
|
-
val text: String,
|
|
1163
|
-
)
|
|
1622
|
+
private companion object {
|
|
1623
|
+
private val PASTE_MIME_TYPES = arrayOf("text/*")
|
|
1624
|
+
}
|
|
1164
1625
|
}
|